大纲
JS中每个对象都有原型
内置属性[[Prototype]]
书中提到:每个对象都有不可枚举的内置属性[[Prototype]]
这里具体化:对象可以通过obj.__proto__
访问到它的原型,或者可以调用方法Object.getPrototypeOf(obj)
访问其原型。
对于一些基础类型的变量,以number
类型举例:
当var a = 1
时,a的类型是number
,当访问a.__proto__
时,JS引擎调用Number
创建新的Number
对象封装a
,因此a.__proto__
是Number
。
对于引用类型object(包括空对象):
对象的原型是Object.prototype
。没错它就叫“Object.prototype”,没有其他别名。
__proto__
与.prototype
辨析
.prototype
是函数特有的属性,例如Foo(){}
函数的prototype
属性是Foo.prototype
。它是原型对象,没有别的别名)
- 通常
__proto__
被称为隐式原型,指向构造该对象的构造函数的原型,构成原型链。 - 通常
.prototype
被称为显示原型,用于实现基于原型的继承与属性共享。
后文中,我们将用隐式原型和显示原型的称呼区分proto和prototype
实际上__proto__
属性并不存在于当前对象,它是Object.prototype的属性,当我们访问obj.__proto__
时,实际上更像是在访问getter/setter:
prototype
是函数自身的属性,原型对象prototype
有个属性constructor
指向函数自身。
当然函数也是对象,也有__proto__
属性。
原型链
原型链是通对象的__proto__
属性串联起来的链(可不是prototype
属性)
原型链是什么样的
继续以Number
类型为例:
通过在控制台上的输出
我们可以总结为下图:
- Number的
__proto__
属性指向其构造函数的prototype
属性,Number的构造函数是Object,因此Number.__ptoto__ == Object.prototype
。 - 对象通过
__proto__
链接到原型,原型对象又通过__proto__
链接到原型的原型从而构成了原型链 - 原型链的终点是
Object.prototype
,(毕竟所有对象都是由Object创建),Object.__proto__
指向null
。
原型链可以循环吗
尝试可以发现__proto__
属性不允许有循环引用
基于原型链查找属性和方法
接着上面例子:var a = 1
,当我们调用a.toString()
(返回"1"
),它的底层工作经历了如下的查找:
- 当前对象本身有没有toString方法(显然没有
- 遍历a的原型链中的对象,知道查找到某个对象含有toString方法为止。(这里沿着a->Number->Object.prototype, 然后在Object.prototype中找到了方法toString,然后返回/调用该方法
- 若遍历到原型链的终点,也没有找到想要的属性/方法,则返回undefined
在第2步返回/调用方法这里有个小细节,我们虽然在Object.prototype
对象上找到对应的方法,但是调用方法的是对象a
,也就是toString方法中的this指向a
, 而非Object.prototype
。
可以沿着原型链查找,但是不能沿着原型链赋值
为对象原来没有的属性赋值,js在当前对象上创建新的属性并为其赋值。
var objA = {a:1}
// 将objA设为objB的原型
var objB = Object.create(objA)
objB.a // 1
objA.a // 1
objB.a = 2 // 会在objB对象上新增属性a并赋值为2
objB.a // 2
objA.a //1
上面的例子,为objB.a赋值后,它将objA.a覆盖,我们就不能通过objB.a访问到objA.a。
其他沿着原型链查找的例子:
for...in
遍历对象,以及key in obj
都会便利[[Prototype]]
查找属性
关联
将b设为a的原型:var objB = Object.create(objA)
Object.create()
该函数底层实现类似于:
function object(o){
function F(){}
F.prototype = o;
return new F()
}
它做了以下几个步骤:
- 创建一个空的构造函数,
- 将传入的对象作为构造函数的显示原型
- 通过构造函数创建新对象,并返回。此时新对象的__proto__属性指向构造函数的显示原型。
因此我们可以总结出,var objB = Object.create(objA)
,就是将建一个新对象,新对象的__proto__
属性指向第一个参数,即objA
,然后将新对象赋值给objB
。
ES6新增方法Object.setPrototype(objB, objA)
也可以达到此效果。
另,如果想要创建一个用于单纯存储数据的变量,而不想其有什么原型链可以Object.create(null)
内省/反射:判断对象是否在原型链上
-
instanceof
:a instanceof Foo
,检查a的整条原型链是否有指向Foo.prototype的对象。 -
isPrototypeOf
:b.isPrototypeOf(c)
, b是否出现在c的原型链中
类 VS 对象关联
思考一个复杂的问题:以下两种委托方式有什么不同?
var objB = Object.create(objA)
var objB.prototype = Object.create(objA.prototype)
-
var objB = Object.create(objA)
:创建新变量objB并关联到objA, 也就是objB.__ptoto__ == objA
。这种方式创建的对象objB没有显示原型
prototype
,只有隐式原型__ptoto__
function Foo () {} var Bar = Object.create(Foo) Bar.hasOwnProperty('prototype') // false Bar.__proto__ == Foo // true
-
var objB.prototype = Object.create(objA.prototype)
,即创建了新的对象覆盖原来的objB.prototype
, 同时objB.prototype.__proto__ == objA.prototype
, 类似于创建了类objB
继承类objA
(只不过类名称的首字母通常大写,这里小写)。function Foo () {} function Bar () {} // Bar需要先声明 Bar.prototype = Object.create(Foo.prototype) Bar.hasOwnProperty('prototype') // true Bar.__proto__ // ƒ() { [native code] }
两种的关系图如下:
左边的代码风格是对象关联风格,右边的代码风格我们称为类风格。不过我们要明确一点js中没有真正的“类”,其底层还是靠委托实现。
以下是我对委托和类的个人理解:
委托:委托的思想是基于原型链的,说白了就是,若objB关联objA(objB.__proto__ == objA
),那么无论是属性还是方法objB上查找不到的,那么就会去objB原型链上的对象上查找。查找到就会访问相应的属性/调用相应的方法(注意this的绑定)。看起来像把对象自身的方法和属性委托给它原型链上的对象。
传统的类的继承:子类继承父类,则子类会复制父类的属性和方法。(而委托只是引用了原型对象的属性/方法)
ES6有语法糖Class,其本质还是通过委托实现,并没实现传统的类
JavaScript中父类和子类的关系只存在于两者构造函数对应的.prototype对象中,构造函数之间并不存在直接联系。
类的代码风格实例
对象之间的关系结构图如下:
对象关联的代码风格实例
对象之间的关系结构图如下:
总结
JS中每个对象都具有[[prototype]]属性,原型属性连接形成原型链。
当我们访问当前对象上不存在的属性/方法时,就会沿着原型链上的对象查找属性/方法,如果找到就返回,否则直到原型链的尽头Object.prototype上都没有找到,就返回undefined。我们称之为委托:将属性/对象委托给原型链上的对象。
JS基于委托实现了面向类的代码风格,es6新增语法糖class,为开发者提供了更简洁可读的面向类代码风格。
但是js底层并没有实现传统的“类”,因为传统类中,子类继承父类是将父类的方法和属性复制,而js底层是通过委托实现。因此作者提供了一种更适合js的代码风格:对象关联。
对象关联代码风格的优势在于:首先它更接近js的实际机制;而且它使代码结构更加简单。