前言
面向对象( Object-Oriented, OO)的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。前面提到过, ECMAScript 中没有类的概念,因此它的对象也与基于类的语言中的对象有所不同。
ECMA-262 把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。”严格来讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。正因为这样(以及其他将要讨论的原因),我们可以把 ECMAScript 的对象想象成散列表:无非就是一组名值对,其中值可以是数据或函数。 ----《javascript高级程序设计(第3版)》
第 4 章中我们说过, JavaScript 和面向类的语言不同, 它并没有类来作为对象的抽象模式
或者说蓝图。 JavaScript 中只有对象。
实际上, JavaScript 才是真正应该被称为“面向对象” 的语言, 因为它是少有的可以不通
过类, 直接创建对象的语言。
在 JavaScript 中, 类无法描述对象的行为,(因为根本就不存在类! ) 对象直接定义自己的行为。 再说一遍, JavaScript 中只有对象。 ----《你不知道的JavaScript(上卷)》
从引用中我们可以看到,JavaScript与传统的OO语言不同,并没有真正的“面向类的程序设计”的概念,对象是JavaScript中十分重要的类型。
JavaScript为我们提供了“模仿类”的实现方案,以便于我们使用经典的OO语言的逻辑来设计和编写代码(也是现在大多数人使用的方案);但同时,我们在了解JavaScript的[[Prototype]]机制后,也可以回归语法本源,使用面向委托的逻辑进行设计。
一、JavaScript中对象的基本概念
JavaScript中有七种内置类型:
- null
- undefined
- boolean
- number
- string
- object
- symbol
其中,object为引用类型,其余为基本类型,在JavaScript中数组(Array)、函数(Function)都属于object类型。
创建 Object 实例的方式有两种:
-
使用 new 操作符后跟 Object 构造函数:
let person = new Object(); person.name = "Bryce"; person.age = 20;
-
对象字面量(对象字面量是对象定义的一种简写形式,目的在于简化创建
包含大量属性的对象的过程):let person = { name : "Bryce", age : 20 };
object类型下常用方法:
- Object.keys()
遍历对象属性,传入的参数是一个对象,返回的是一个数组,数组中包含的是该对象所有的属性名 - Object.values()
返回一个数组,数组中为参数对象自身的(不含原型链上的)所有可遍历( enumerable )属性的键值(如果参数不是对象,Object.values会先将其转为对象。由于number和boolean的包装对象,都不会为实例添加非继承的属性,所以Object.values会返回空数组;而string会先转成一个类似数组的对象,字符串的每个字符,就是该对象的一个属性。因此,Object.values返回每个属性的键值,就是各个字符组成的一个数组) - Object.create()
创建一个新对象,使用现有的对象来提供新创建的对象的__proto__ - Object.hasOwnProperty()
判断对象自身(不含原型链上的)属性中是否具有指定的属性 - Object.getOwnPropertyNames()
返回对象的所有自身属性的属性名(包括不可枚举的属性)组成的数组,但不会获取原型链上的属性 - Object.defineProperty()
设置对象的数据属性或访问器属性 - Object.assign()
合并对象,将源对象( source )的所有可枚举属性,复制到目标对象( target ),如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性(可以用symbol类型做key解决这个问题) - Object.getPrototypeOf()
返回指定对象的原型(内部[[Prototype]]属性的值) - Object.setPrototypeOf()
设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或 null(性能不及Object.create())
理解Object.defineProperty():
object的属性:
-
数据属性
数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有 4 个描述其行为的特性。
[[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。默认值为 true。
[[Enumerable]]:表示能否通过 for-in 循环返回属性。默认值为 true。
[[Writable]]:表示能否修改属性的值。默认值为 true。
[[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。默认值为 undefined。//使用Object.defineProperty()修改数据属性的例子 var person = {}; Object.defineProperty(person, "name", { writable: false, value: "Bryce" }); alert(person.name); //"Bryce" person.name = "Greg"; alert(person.name); //"Bryce"
-
访问器属性
访问器属性不包含数据值;它们包含一对儿 getter 和 setter 函数(不过,这两个函数都不是必需的)。在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter 函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下 4 个特性。
[[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。默认值为true。
[[Enumerable]]:表示能否通过 for-in 循环返回属性。默认值为 true。
[[Get]]:在读取属性时调用的函数。默认值为 undefined。
[[Set]]:在写入属性时调用的函数。默认值为 undefined。//使用Object.defineProperty()修改访问器属性的例子 var book = { _year: 2004, edition: 1 }; Object.defineProperty(book, "year", { get: function(){ return this._year; }, set: function(newValue){ if (newValue > 2004) { this._year = newValue; this.edition += newValue - 2004; } } }); book.year = 2005; alert(book.edition); //2
二、使用JS对象前要了解什么
1.“模仿类”带来的误导
有些语言(比如 Java) 并不会给你选择的机会, 类并不是可选的——万物皆是类。 其他语言(比如 C/C++ 或者 PHP) 会提供过程化和面向类这两种语法, 开发者可以选择其中一种风格或者混用两种风格。
JavaScript 属于哪一类呢? 在相当长的一段时间里, JavaScript 只有一些近似类的语法元素(比如 new 和 instanceof), 不过在后来的 ES6 中新增了一些元素, 比如 class 关键字。
这是不是意味着 JavaScript 中实际上有类呢? 简单来说: 不是。
由于类是一种设计模式, 所以你可以用一些方法(本章之后会介绍) 近似实现类的功能。为了满足对于类设计模式的最普遍需求, JavaScript 提供了一些近似类的语法。
虽然有近似类的语法, 但是 JavaScript 的机制似乎一直在阻止你使用类设计模式。 在近似类的表象之下, JavaScript 的机制其实和类完全不同。 语法糖和( 广泛使用的)JavaScript“类” 库试图掩盖这个现实, 但是你迟早会面对它: 其他语言中的类和 JavaScript中的“类” 并不一样。 ----《你不知道的JavaScript(上卷)》
传统的类被实例化时,他的行为会被复制到实例中;类被继承时,行为也会被复制到子类中;多态看起来似乎是从子类引用父类,但是本质上引用的其实是复制的结果。
但是,JavaScript并不会像类那样自动创建对象的副本。
就算不使用面向委托的设计,在JavaScript “模仿类”的过程中,“实例化”是在新建的对象上调用“构造函数”,并创建关联;“继承”只是在两个对象之间创建一个关联,一个对象可以通过这层“关联”访问到另一个对象的方法和属性,这层“关联”就是原型链。
然而,“类”、“继承”、“构造函数”、“实例”、“多态”等这样令人容易混淆的术语也经常出现在JavaScript中,但是我们要知道,这些经典的OO语言术语是为了方便我们理解JavaScript中类似的操作的设计思路,而并不代表实际的实现机制。
——本文也会多次出现这些术语,但请再次注意,他们在JavaScript中的实现机制和传统OO语言并不相同
2.new
new可以帮助我们在JavaScript中创建一个新“实例”,其实现原理如下:
//模拟一个new
function myNew(constrc, ...args) {
const obj = {}; // 1. 创建一个空对象
obj.__proto__ = constrc.prototype; // 2. 将obj的[[prototype]]属性指向构造函数的原型对象(进行了关联)
constrc.apply(obj, args); // 3.将constrc执行的上下文this绑定到obj上,并调用
return obj; //4. 返回新创建的对象
}
// 使用的例子:
//创建“构造函数”Person
function Person(name, age){
this.name = name;
this.age = age;
}
const person1 = myNew(Person, 'Tom', 20);
console.log(person1); // Person {name: "Tom", age: 20}
这里的“构造函数”并不是真正的构造函数,只是一个普通的函数,只是可以把通过new来进行调用的函数暂且称为“构造函数”(实现了类似其他OO语言中构造函数的功能)
3.原型链
让我们通过例子来了解下原型链的概念,以及顺便看看原型链是如何帮助我们实现对“类”的模拟的:
首先创建一个Object“实例”:
let obj = new Object();
用这张图片来理解其中的几个概念:
- prototype属性:
prototype属性是函数所独有的,从一个函数指向一个对象,含义是函数的原型对象;作用是创建可查找由特定类型的所有实例共享的属性和方法的关联,也就是让该函数所实例化的对象们都可以找到公用的属性和方法。任何函数在创建的时候,会默认同时创建该函数的prototype对象。 - __proto__属性:
__proto__属性是对象所独有的(函数作为对象也拥有该属性),__proto__属性都是由一个对象指向一个对象,即指向它们的原型对象;它的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向的那个对象里找,如果父对象也不存在这个属性,则继续往父对象的__proto__属性所指向的那个对象里找,如果还没找到,则继续往上找…直到原型链顶端null,这种通过__proto__属性来连接对象直到null的一条链即为我们所谓的原型链。 - constructor属性:
constructor属性也是对象所独有的(函数作为对象也拥有该属性),但实际上constructor本身只是在某个函数被声明时产生的默认属性,对象(这里一般指xxx.prototype)的.constructor属性默认指向一个函数,而这个函数也有一个.prototype的引用指向这个对象,这种关联其实是松散的,因为constructor并不是一个不可变属性,它的值可以任意修改,而且当使用新创建的对象赋予xxx.prototype时,.constructor也会丢失,所以constructor是不可信赖的,虽然通过new()产生的对象可以访问.constructor属性,并在一般情况下访问到“构造”出该对象的函数,但是其实这个constructor属性并不是对象自己的,而是通过原型链[[Prototype]]的默认委托访问到的,不能把constructor的指向理解为“…由…构造”(因为关联很容易被破坏),且最好避免使用这个引用。Function这个对象比较特殊,它的构造函数就是它自己(因为Function可以看成是一个函数,也可以是一个对象),所有函数和对象最终都是由Function构造函数得来,所以constructor属性的终点就是Function这个函数。
接下来来看一个我们经常会遇到的情况:
function Student() {/*这里是一个“构造函数”*/};
let student1 = new Student();
通过刚刚的理解,我们就能比较清楚地画出原型链图解了(为了不过于混乱,省略了上图中虚线框中的部分):
4.Object.create()
方法说明:
- Object.create()方法创建一个新的对象,并以方法的第一个参数作为新对象的__proto__属性的值(以第一个参数作为新对象的构造函数的原型对象)
- Object.create()方法还有第二个可选参数,传入一个对象,对象的每个属性都会作为创建出的新对象的自身属性,且enumerable默认为false
Object.myCreate = function (proto, propertyObject = undefined) {
if (propertyObject === null) {
throw 'TypeError';
} else {
function F (){}
Fn.prototype = proto;
const obj = new F();
if (propertyObject !== undefined) {
Object.defineProperties(obj, propertyObject);
}
if (proto === null) {
// 创建一个没有原型对象的对象
obj.__proto__ = null;
}
return obj;
}
}
Object.create()会创建一个新对象并将其关联到我们指定的对象,这样我们就可以充分发挥原型链机制的威力(委托)并且避免使用new可能带来的一些麻烦,通过Object.create()创建出的关联关系是纯粹的关联关系(后续会进行使用)。
Object.create(null) 会 创 建 一 个 拥 有 空( 或 者 说 null) [[Prototype]]链接的对象, 这个对象无法进行委托。 由于这个对象没有原型链, 所以instanceof 操作符( 之前解释过) 无法进行判断, 因此总是会返回 false。这些特殊的空 [[Prototype]] 对象通常被称作“字典”, 它们完全不会受到原型链的干扰, 因此非常适合用来存储数据 ----《你不知道的JavaScript(上卷)》
总结
本文分析了JavaScript对象的一些基本概念理论和容易产生误解的地方,在我们理解了JS作为面向对象语言对模仿“类”设计模式的运用与支持,以及由于其独特的“原型链”结构所导致的与其他面向对象语言十分不同的实现方案后,我们才能更清楚地去分析JS中各种创建对象的模式,ES6中的Class语法以及实现“继承”的模式等。
在下半段文章中将继续分析上述内容,以及如何回归语法本源,使用面向委托的设计,在不借助“类”的设计模式下完成相同的工作。
后篇
参考书籍:《JavaScript高级程序设计(第3版)》、《你不知道的JavaScript(上卷)》
版权声明:本文为博主原创文章,转载请在显著位置标明本文出处以及作者昵称,未经作者允许不得用于商业目的。