JS基础之深入constructor-prototype-__proto__-[[Prototype]]及原型链
本篇文章主讲构造函数、原型以及原型链,包括 Symbol
是不是构造函数、constructor
属性是否只读、prototype
、__proto__
、[[Prototype]]
、原型链。
一、基础入门
1. 对象
在JS中,万物皆对象,对象又分为普通对象和函数对象,其中 Object、Function 为 JS 自带的函数对象。
let obj1 = {}; |
凡是通过 new Function()
创建的对象都是函数对象,其他的都是普通对象,Function Object 是通过 New Function()
创建的。
2. 构造函数
function Foo(name, age) { |
每个实例都有一个 constructor
(构造函数)属性,该属性指向对象本身。
f.constructor === Foo // true |
构造函数本身就是一个函数,与普通函数没有任何区别,不过为了规范一般将其首字母大写。构造函数和普通函数的区别在于,使用 new
生成实例的函数就是构造函数,直接调用的就是普通函数。
JS 本身不提供一个 class
实现。(在 ES2015/ES6 中引入了 class
关键字,但只是语法糖,JavaScript 仍然是基于原型的)。
3. 构造函数扩展
let a = {}
其实是let a = new Object()
的语法糖let a = []
其实是let a = new Array()
的语法糖function Foo(){ ... }
其实是var Foo = new Function(...)
- 可以使用
instanceof
判断一个函数是否为一个变量的构造函数
4. Symbol 是构造函数吗?
Symbol 是基本数据类型,它并不是构造函数,因为它不支持 new Symbol()
语法。我们直接使用Symbol()
即可。
let an = Symbol("An"); |
但是,Symbol()
可以获取到它的 constructor 属性
Symbol("An").constructor; |
这个 constructor
实际上是 Symbol 原型上的,即
Symbol.prototype.constructor; |
对于 Symbol,你还需要了解以下知识点:
Symbol()
返回的 symbol 值是唯一的
Symbol("An") === Symbol("An"); |
可以通过 Symbol.for(key)
获取全局唯一的 symbol
Symbol.for('An') === Symbol.for("An"); // true |
它从运行时的 symbol 注册表中找到对应的 symbol,如果找到了,则返回它,否则,新建一个与该键关联的 symbol,并放入全局 symbol 注册表中。
Symbol.iterator :返回一个对象的迭代器
// 实现可迭代协议,使迭代器可迭代:Symbol.iterator |
Symbol.toPrimitive:将对象转换成基本数据类型
// Symbol.toPrimitive 来实现拆箱操作(ES6 之后) |
Symbol.toStringTag:用于设置对象的默认描述字符串值
// Symbol.toStringTag 代替 [[class]] 属性(ES5开始) |
5. constructor 的值是只读的吗?
对于引用类型来说 constructor
属性值是可以修改的,但是对于基本类型来说是只读的。
引用类型
function An() { |
这说明,依赖一个引用对象的 constructor 属性,并不是安全的。
基本类型
function An() {}; |
这是因为:原生构造函数(native constructors
)是只读的。
JS 对于不可写的属性值的修改静默失败(silently failed),但只会在严格模式下才会提示错误。
'use strict'; |
注意:null
和 undefined
是没有 constructor
属性的。
二、原型
首先,贴上
图片来自于http://www.mollypages.org/tutorials/js.mp,请根据下文仔细理解这张图
在JS中,每个对象都有自己的原型。当我们访问对象的属性和方法时,JS 会先访问对象本身的方法和属性。如果对象本身不包含这些属性和方法,则访问对象对应的原型。
// 构造函数 |
1. prototype
所有函数都有一个 prototype
(显式原型)属性,属性值也是一个普通的对象。对象以其原型为模板,从原型继承方法和属性,这些属性和方法定义在对象的构造器函数的 prototype
属性上,而非对象实例本身。
但有一个例外: Function.prototype.bind()
,它并没有 prototype 属性
let fun = Function.prototype.bind(); |
当我们创建一个函数时,例如
function Foo () {} |
prototype
属性就被自动创建了
从上面这张图可以发现,Foo
对象有一个原型对象 Foo.prototype
,其上有两个属性,分别是 constructor
和 __proto__
,其中 __proto__
已被弃用。
构造函数 Foo
有一个指向原型的指针,原型 Foo.prototype
有一个指向构造函数的指针 Foo.prototype.constructor
,这就是一个循环引用,即:
Foo.prototype.constructor === Foo; // true |
2. __proto__
每个实例对象(object )都有一个隐式原型属性(称之为 __proto__
)指向了创建该对象的构造函数的原型。也就时指向了函数的 prototype
属性。
function Foo () {} |
当 new Foo()
时,__proto__
被自动创建。并且
foo.__proto__ === Foo.prototype; // true |
即:
__proto__
发音 dunder proto,最先被 Firefox使用,后来在 ES6 被列为 Javascript 的标准内建属性。
3. [[Prototype]]
[[Prototype]]
是对象的一个内部属性,外部代码无法直接访问。
遵循 ECMAScript 标准,someObject.[[Prototype]] 符号用于指向 someObject 的原型
4. 注意
__proto__
属性在 ES6
时才被标准化,以确保 Web 浏览器的兼容性,但是不推荐使用,除了标准化的原因之外还有性能问题。为了更好的支持,推荐使用 Object.getPrototypeOf()
。
通过现代浏览器的操作属性的便利性,可以改变一个对象的
[[Prototype]]
属性, 这种行为在每一个JavaScript引擎和浏览器中都是一个非常慢且影响性能的操作,使用这种方式来改变和继承属性是对性能影响非常严重的,并且性能消耗的时间也不是简单的花费在obj.__proto__ = ...
语句上, 它还会影响到所有继承来自该[[Prototype]]
的对象,如果你关心性能,你就不应该在一个对象中修改它的 [[Prototype]]。相反, 创建一个新的且可以继承[[Prototype]]
的对象,推荐使用Object.create()
。
如果要读取或修改对象的 [[Prototype]]
属性,建议使用如下方案,但是此时设置对象的 [[Prototype]]
依旧是一个缓慢的操作,如果性能是一个问题,就要避免这种操作。
// 获取(两者一致) |
如果要创建一个新对象,同时继承另一个对象的 [[Prototype]]
,推荐使用 Object.create()
。
function An() {}; |
这里 anran
是一个新的空对象,有一个指向对象 an
的指针 __proto__
。
5. new 的实现过程
- 新生成了一个对象
- 链接到原型
- 绑定 this
- 返回新对象
function new_object() { |
优化 new 实现
// 优化后 new 实现 |
6. 总结
- 所有的引用类型(数组、对象、函数)都有对象特性,即可自由扩展属性(null除外)。
- 所有的引用类型,都有一个
__proto__
属性,属性值是一个普通的对象,该原型对象也有一个自己的原型对象(__proto__
) ,层层向上直到一个对象的原型对象为null
。根据定义,null
没有原型,并作为这个原型链 中的最后一个环节。 - 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的
__proto__
(即它的构造函数的prototype
)中寻找。
三、原型链
每个对象拥有一个原型对象,通过 __proto__
指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null
,这种关系被称为原型链(prototype chain)。根据定义,null
没有原型,并作为这个原型链中的最后一个环节。
原型链的基本思想是利用原型,让一个引用类型继承另一个引用类型的属性及方法。
// 构造函数 |
f.__proto__=== Foo.prototype
,Foo.prototype
也是一个对象,也有自己的__proto__
指向 Object.prototype
, 找到toString()
方法。
也就是
Function.__proto__.__proto__ === Object.prototype |
下面是原型链继承的例子
function Elem(id) { |
四、总结
Symbol
是基本数据类型,并不是构造函数,因为它不支持语法new Symbol()
,但其原型上拥有constructor
属性,即Symbol.prototype.constructor
。- 引用类型
constructor
是可以修改的,但对于基本类型来说它是只读的,null
和undefined
没有constructor
属性。 __proto__
是每个实例对象都有的属性,prototype
是其构造函数的属性,在实例上并不存在,所以这两个并不一样,但foo.__proto__
和Foo.prototype
指向同一个对象。__proto__
属性在ES6
时被标准化,但因为性能问题并不推荐使用,推荐使用Object.getPrototypeOf()
。- 每个对象拥有一个原型对象,通过
__proto__
指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层向上,最终指向null
,这就是原型链。 - 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的原型中寻找,以及该对象的原型的原型,一层一层向上查找,直到找到一个名字匹配的属性 / 方法或到达原型链的末尾(
null
)