JavaScript对象基础
对象基础
对象是一个包含相关数据和方法的集合(通常由一些变量和函数组成,我们称之为对象里面的属性和方法)。
1 | const person = { |
一个如上所示的对象被称之为对象字面量(object literal)——手动的写出对象的内容来创建一个对象。
当你想要传输一系列结构化的相关的数据项(例如,服务器发起请求以存储一些数据到数据库)时,常见的方式是使用字面量来创建一个对象。
点表示法
使用点表示法(dot notation)来访问对象的属性和方法。对象的名字表现为一个命名空间(namespace)。当你想访问对象内部的属性或方法时,命名空间必须写在第一位。然后输入一个点,紧接着是你想要访问的目标——可以是简单属性的名字,或者是数组属性的一个子元素,又或者是对象的方法调用。如下所示:
1 | person.age; |
子命名空间
用一个对象来做另一个对象成员的值。例如将 name 成员
1 | const person = { |
需要访问这些属性,只需要链式的再使用一次点表示法,像这样:
1 | person.name.first; |
括号表示法
另外一种访问对象属性的方式是使用括号表示法(bracket notation)
1 | person.age; |
- 这看起来很像访问一个数组的元素,并且基本上是相同的——使用关联了值的名称,而不是索引来选择元素。因此对象有时被称为关联数组——对象将字符串映射到值,而数组将数字映射到值。
点表示法通常优于括号表示法,因为它更简洁且更易于阅读。然而,在某些情况下你必须使用括号。例如,如果对象属性名称保存在变量中,则不能使用点表示法访问该值,但可以使用括号表示法访问该值。
在下面的示例中,logProperty() 函数可以使用 person[propertyName] 来检索 propertyName 中指定的属性的值。
1 | const person = { |
设置对象成员
目前我们仅仅看到了如何查询(或获取)对象成员,而且也可以通过声明设置(更新)对象成员的值(使用点表示法或括号表示法),像这样:
1 | person.age = 45; |
设置成员并不意味着你只能更新已经存在的属性的值和方法,你也可以创建新的成员。
1 | person["eyes"] = "hazel"; |
括号表示法一个有用的地方是它不仅可以动态的去设置对象成员的值,还可以动态的去设置成员的名字。假设我们想让用户能够通过在两个文本输入框中键入成员名称和值,在他们的人员数据中存储自定义的值类型。我们可以像这样获取这些值:
1 | const myDataName = nameInput.value; |
- 这是使用点表示法无法做到的,点表示法只能接受字面量的成员的名字,不接受表示名称的变量。
this 的含义
当你只需要创建一个对象字面量时,this 就不是那么有用。但是如果你创建多个对象时,this 可以让你对每一个创建的对象都使用相同的方法定义。
1 | const person1 = { |
在本例中,尽管两个实例的方法代码完全相同,但 person1.introduceSelf() 输出“你好!我是 Chris。”,而 person2.introduceSelf() 输出“你好!我是 Deepti”。当你手工编写对象字面量时,这并不是很有用,但是当我们开始使用构造函数从单个对象定义创建多个对象时,这将是必不可少的。
this 的规则:
- 通过 new 创建的对象,this 指向 new 创建的对象。
- call/apply/bind 方法可以操作我们给定的对象,显示绑定。
- this 放在方法中,this 指向调用这个方法的对象,隐式绑定。
- 以上三种都不是,严格模式下 this 指向undefined,非严格模式下 this 指向 window。
- 箭头函数没有 this,this 从外部获取。
this 指向
- 在绝大多数情况下,函数的调用方式决定了 this 的值。this 不能在执行期间被赋值,并且在每次函数被调用时 this 的值也可能会不同。
- 当前执行上下文的一个属性,在非严格模式下,总是指向一个对象,在严格模式下可以是任意值。
- 无论是否在严格模式下,在全局执行环境中(在任何函数体外部)this 都指向全局对象。
1 | // 在浏览器中,window 对象同时也是全局对象: |
在函数内部,this的值取决于函数被调用的方式。
- 因为下面的代码不在严格模式下,且 this 的值不是由该调用设置的,所以 this 的值默认指向全局对象,浏览器中就是 window。
1 | function f1(){ |
- 然而,在严格模式下,如果进入执行环境时没有设置 this 的值,this 会保持为 undefined,如下:
1 | function f2(){ |
- 如果要想把 this 的值从一个环境传到另一个,就要用 call 或者 apply 方法。
1 | // 对象可以作为 bind 或 apply 的第一个参数传递,并且该参数将绑定到该对象。 |
- 在非严格模式下使用 call 和 apply 时,如果用作 this 的值不是对象,则会被尝试转换为对象。
- null 和 undefined 被转换为全局对象。
1 | function bar() { |
- 当函数作为对象里的方法被调用时,this 被设置为调用该函数的对象。
1 | var o = { |
- 对于在对象原型链上某处定义的方法,同样的概念也适用。如果该方法存在于一个对象的原型链上,那么 this 指向的是调用这个方法的对象,就像该方法就在这个对象上一样。
1 | var o = { |
构造函数
构造函数只是使用 new 关键字调用的函数。当你调用构造函数时,它将:
- 创建一个新对象
- 将 this 绑定到新对象,以便你可以在构造函数代码中引用 this
- 运行构造函数中的代码
- 返回新对象
构造函数以大写字母开头,并且以它们创建的对象类型命名。
1 | function Person(name) { |
对象原型
原型链
JavaScript 中所有的对象都有一个内置属性,称为它的 prototype(原型)。它本身是一个对象,故原型对象也会有它自己的原型,逐渐构成了原型链。原型链终止于拥有 null 作为其原型的对象上。
注: 指向对象原型的属性并不是 prototype。它的名字不是标准的,但实际上所有浏览器都使用 __proto__。访问对象原型的标准方法是 Object.getPrototypeOf()。
myObject 的原型是什么?为了找到答案,我们可以使用 Object.getPrototypeOf() 函数:
1 | Object.getPrototypeOf(myObject); // Object { } |
有个对象叫 Object.prototype,它是最基础的原型,所有对象默认都拥有它。Object.prototype 的原型是 null,所以它位于原型链的终点:
一个对象的原型并不总是 Object.prototype,试试这段代码:
1 | const myDate = new Date(); |
这段代码创建了 Date 对象,然后遍历了它的原型链,记录并输出了原型。从中我们知道 myDate 的原型是 Date.prototype 对象,它(Date.prototype)的原型是 Object.prototype。

属性遮蔽
如果你在一个对象中定义了一个属性,而在该对象的原型中定义了一个同名的属性,会发生什么?
1 | const myDate = new Date(1995, 11, 17); |
鉴于对原型链的描述,这应该是可以预测的。当我们调用 getYear() 时,浏览器首先在 myDate 中寻找具有该名称的属性,如果 myDate 没有定义该属性,才检查原型。因此,当我们给 myDate 添加 getYear() 时,就会调用 myDate 中的版本。这个方法会遮蔽掉原型链上的同名方法,这叫做属性的“遮蔽”。
设置原型
在 JavaScript 中,有多种设置对象原型的方法,这里我们将介绍两种:Object.create() 和构造函数。
使用 Object.create
Object.create() 方法创建一个新的对象,并允许你指定一个将被用作新对象原型的对象。
1 | const personPrototype = { |
这里我们创建了一个 personPrototype 对象,它有一个 greet() 方法。然后我们使用 Object.create() 来创建一个以 personPrototype 为原型的新对象。现在我们可以在新对象上调用 greet(),而原型提供了它的实现。
使用构造函数
在 JavaScript 中,所有的函数都有一个名为 prototype 的属性。当你调用一个函数作为构造函数时,这个属性被设置为新构造对象的原型(按照惯例,在名为 proto 的属性中)。
因此,如果我们设置一个构造函数的 prototype,我们可以确保所有用该构造函数创建的对象都被赋予该原型:
1 | const personPrototype = { |
- 创建了一个 personPrototype 对象,它具有 greet() 方法
- 创建了一个 Person() 构造函数,它初始化了要创建人物对象的名字
使用 Object.assign 将 personPrototype 中定义的方法绑定到 Person 函数的 prototype 属性上。
在这段代码之后,使用 Person() 创建的对象将获得 Person.prototype 作为其原型,其中自动包含 greet 方法。
1 | const reuben = new Person("Reuben"); |
自有属性
我们使用上面的 Person 构造函数创建的对象有两个属性:
- name 属性,在构造函数中设置,在 Person 对象中可以直接看到
- greet() 方法,在原型中设置
我们经常看到这种模式,即方法是在原型上定义的,但数据属性是在构造函数中定义的。这是因为方法通常对我们创建的每个对象都是一样的,而我们通常希望每个对象的数据属性都有自己的值(就像这里每个人都有不同的名字)。
直接在对象中定义的属性,如这里的name,被称为自有属性,你可以使用静态方法 Object.hasOwn() 检查一个属性是否是自有属性:
1 | const irma = new Person("Irma"); |
注: 你也可以在这里使用非静态方法 Object.hasOwnProperty(),但我们推荐尽可能使用 Object.hasOwn() 方法。
面向对象
类与实例
类的定义包括了属性和方法。
1 | class Professor |
类可以创建实例。(我们称这样创建出来的具体的人为类的实例)。
类创建实例的过程是由一个特别的函数——构造函数所完成的。
通常来说,需要将构造函数作为类定义的一部分明确声明,并且构造函数通常具有和类名相同的函数名。
1 | class Professor |
通常使用 new 关键字来表示执行构造函数。
1 | walsh = new Professor("沃尔什", "心理学"); |
这段代码中我们创建了两个对象,这两个对象都是 Professor 类的实例。
继承
容易注意到教授和学生都是人,而人是具有姓名,并且可以介绍自己的。我们可以将人定义为一个新类,即 Person 类,在 Person 类中,我们可以定义所有作为人的通用属性。接下来,我们可以定义 Professor 类和 Student 类由 Person 类派生(derive)而来,在伪代码中定义如下:
1 | class Person |
在这种情况下,我们称 Person 类是 Professor 类和 Student 类的超类(superclass)或父类(parent class)。
反之,我们称 Professor 类和 Student 类是 Person 类的子类(subclass 或 child class)。
你可能注意到了我们在三个类中都定义了 introduceSelf() 方法。这么做的原因如下:尽管所有人都想要介绍他们自己,但是他们可能会以不同的方式去做这件事。
1 | walsh = new Professor("沃尔什", "心理学"); |
我们可能会为那些不是教授或学生的人设定一个默认的打招呼方式:
1 | pratt = new Person("普拉特"); |
当一个方法拥有相同的函数名,但是在不同的类中可以具有不同的实现时,我们称这一特性为多态(polymorphism)。当一个方法在子类中替换了父类中的实现时,我们称之为子类重写/重载(override)了父类中的实现。
封装
当其他部分的代码想要执行对象的某些操作时,可以借助对象向外部提供的接口完成操作,借此,对象保持了自身的内部状态不会被外部代码随意修改。也就是说,对象的内部状态保持了私有性,而外部代码只能通过对象所提供的接口访问和修改对象的内部状态,不能直接访问和修改对象的内部状态。保持对象内部状态的私有性、明确划分对象的公共接口和内部状态,这些特性称之为封装(encapsulation)。
封装的好处在于,当程序员需要修改一个对象的某个操作时,程序员只需要修改对象对应方法的内部实现即可,而不需要在所有代码中找出该方法的所有实现,并逐一修改。某种意义上来说,封装在对象内部和对象外部设立了一种特别的“防火墙”。
在许多面向对象编程语言中,我们可以使用 private 关键字标记对象的私有部分,也就是外部代码无法直接访问的部分。如果一个属性在被标记为 private 的情况下,外部代码依旧尝试访问该属性,那么通常来说,计算机会抛出一个错误。
1 | class Student : extends Person |
也有部分语言并不采用强制措施阻止外部代码访问对象的私有属性,在这种情况下,程序员们通常会采用一些约定俗称的命名方式来标记对象的私有部分,例如将以下划线开头的变量名看作是对象的私有部分。
面向对象编程与 JavaScript
- 构造函数:在 JavaScript 中,构造函数可以实现类的定义,帮助我们在一个地方描述类的“形状”,包括定义类的方法。不过,原型也可以用于实现类的定义。例如,如果一个方法定义于构造函数的 prototype 属性中,那么所有由该构造函数创造出来的对象都可以通过原型使用该方法,而我们也不再需要将它定义在构造函数中。
- 原型链:原型链很自然地实现了继承特性。例如,如果我们由 Person 原型构造了一个 Student 类,那么我们可以继承 Person 类的 name 属性,重写 introduceSelf() 方法。
理解 JavaScript 的这一对特性与基于类的面向对象编程之间有什么不同,这一点也是十分重要的,这里我们将简要探讨二者的区别。
首先,在基于类的面向对象编程中,类与对象是两个不同的概念,对象通常是由类创造出来的实例。由此,定义类的方式(定义类的语法)和实例化对象的方式(构造函数)也是不同的。而在 JavaScript 中,我们经常会使用函数或对象字面量创建对象,也就是说,JavaScript 可以在没有特定的类定义的情况下创建对象。相对于基于类的面向对象编程来说,这种方式更为轻量,帮助我们更为方便地使用对象。
其次,尽管原型链看起来很像是继承的层级结构,并且在某些方面,原型链的行为与继承的行为也很类似,但是在其他方面,二者之间仍然存在区别。在继承方式下,当一个子类完成继承时,由该子类所创建的对象既具有其子类中单独定义的属性,又具有其父类中定义的属性(以及父类的父类,依此类推)。而在原型链中,每一个层级都代表了一个不同的对象,不同的对象之间通过 proto 属性链接起来。原型链的行为并不太像是继承,而更像是委派(delegation)。委派同样是对象中的一种编程模式。当我们要求对象执行某项任务时,在委派模式下,对象可以自己执行该项任务,或者要求另一个对象(委派的对象)以其自己的方式执行这项任务。在许多方面,相对于继承来说,委派可以更为灵活地在许多对象之间建立联系(例如,委派模式可以在程序运行时改变、甚至完全替换委派对象)。
尽管如此,构造函数和原型仍然可以在 JavaScript 中实现基于类的面向对象编程特性。但是直接使用构造函数和原型去实现这些特性(例如继承)仍是棘手的,因此,JavaScript 提供了一些额外的特性,这些特性在原型这一模型之上又抽象出一层模型,将基于类的面向对象编程中的概念映射到原型中,从而能够更为直接地在 JavaScript 中使用基于类的面向对象编程中的概念。
JavaScript 中的类
类和构造函数
你可以使用 class 关键字声明一个类。
1 | class Person { |
在这个 Person 类的声明中,有:
- 一个 name 属性。
- 一个需要 name 参数的构造函数,这一参数用于初始化新的对象的 name 属性。
- 一个 introduceSelf() 方法,使用 this 引用了对象的属性。
name; 这一声明是可选的:你可以省略它,因为在构造函数中的 this.name = name; 这行代码会在初始化 name 属性前自动创建它。但是,在类声明中明确列出属性可以方便阅读代码的人更容易确定哪些属性是这个类的一部分。
你也可以在声明属性时,为其初始化一个默认值。就像这样:name = ‘’;。
构造函数使用 constructor 关键字来声明。就像在类声明外的构造函数一样,它会:
- 创建一个新的对象
- 将 this 绑定到这个新的对象,你可以在构造函数代码中使用 this 来引用它
- 执行构造函数中的代码
- 返回这个新的对象
如上文中给出的类声明的代码,你可以像这样创建和使用一个新的 Person 实例:
1 | const giles = new Person("Giles"); |
注意,我们使用类的名字来调用构造函数,即示例中的 Person。
如果你不需要任何特殊的初始化内容,你可以省略构造函数,默认的构造函数会被自动生成
继承
使用 extends 关键字来声明这个类继承自另一个类。
1 | class Professor extends Person { |
我们为 Professor 类添加了一个新的属性 teaches,就像声明的那样。
因为我们想在创建新的 Professor 时设置 teaches,我们需要声明一个需要 name 和 teaches 参数的构造函数。构造函数中需要做的第一件事是使用 super() 调用父类的构造函数,并传递 name 参数。父类的构造函数会设置 name 属性。然后 Professor 的构造函数接着设置 teaches 属性。
注: 如果子类有任何自己的初始化内容需要完成,它也必须先使用 super() 来调用父类的构造函数,并传递父类构造函数期望的任何参数。
封装
1 | class Student extends Person { |
在这个类的声明中,#year 是一个私有数据属性。我们可以构造一个 Student 对象,然后在内部使用 #year,但如果在类的外部尝试访问 #year,浏览器将会抛出错误:
1 | const summers = new Student("Summers", 2); |
私有数据属性必须在类的声明中声明,而且其名称需以 # 开头。
与私有数据属性一样,你也可以声明私有方法。而且名称也是以 # 开头,只能在类自己的方法中调用:
1 | class Example { |
JSON
什么是 JSON?
JavaScript 对象表示法(JSON)是用于将结构化数据表示为 JavaScript 对象的标准格式,通常用于在网站上表示和传输数据(例如从服务器向客户端发送一些数据,因此可以将其显示在网页上)。
JSON 是一种按照 JavaScript 对象语法的数据格式,这是道格拉斯·克罗克福特推广的。虽然它是基于 JavaScript 语法,但它独立于 JavaScript,这也是为什么许多程序环境能够读取(解读)和生成 JSON。
JSON 可以作为一个对象或者字符串存在,前者用于解读 JSON 中的数据,后者用于通过网络传输 JSON 数据。这不是一个大事件——JavaScript 提供一个全局的 可访问的 JSON 对象来对这两种数据进行转换。
注: 将字符串转换为原生对象称为反序列化(deserialization),而将原生对象转换为可以通过网络传输的字符串称为序列化(serialization)。
一个 JSON 对象可以被储存在它自己的文件中,这基本上就是一个文本文件,扩展名为 .json,还有 application/json。
JSON 结构
JSON 是一个字符串,其格式非常类似于 JavaScript 对象字面量的格式。
可以在 JSON 中包含与标准 JavaScript 对象相同的基本数据类型——字符串、数字、数组、布尔值和其他对象字面量。
1 | { |
如果我们把字符串加载到 JavaScript 程序中,并将其解析到一个名为 superHeroes 的变量,那么我们就可以使用点/括号表示法来访问其中的数据。例如:
1 | superHeroes.hometown; |
为了访问层次结构中更深层次的数据,必须将所需的属性名和数组索引链接在一起。例如,访问 members 数组第二个英雄的第三个超能力,可以这样做:
1 | superHeroes["members"][1]["powers"][2]; |
JSON 数组
JSON 文本基本上看起来像字符串中的 JavaScript 对象。我们也可以将数组与 JSON 相互转换。下面也是有效的 JSON,例如:
1 | [ |
上面是完全合法的 JSON。你只需要通过数组索引就可以访问数组元素,如 [0][“powers”][0]。
- JSON 是一种纯数据格式,它只包含属性,没有方法。
- JSON 要求在字符串和属性名称周围使用双引号。单引号无效。
- 甚至一个错位的逗号或分号就可以导致 JSON 文件出错。你应该小心的检查你想使用的数据(虽然计算机生成的 JSON 很少出错,只要生成程序正常工作)。你可以通过像 JSONLint 这样的应用程序来验证 JSON。
- JSON 实际上可以是任何可以有效包含在 JSON 中的数据类型的形式。比如,单个字符串或者数字就是有效的 JSON 对象。
- 与 JavaScript 代码中对象属性可以不加引号不同,JSON 中只有带引号的字符串可以用作属性。
对象和文本间的转换
- parse():以文本字符串形式接受 JSON 对象作为参数,并返回相应的对象。(字符串——>对象:反序列化)
- stringify():接收一个对象作为参数,返回一个对应的 JSON 字符串。(对象——>字符串:序列化)



