共计 3276 个字符,预计需要花费 9 分钟才能阅读完成。
[[Prototype]]
JavaScript 中的对象(函数也是对象)有一个非凡的 [[Prototype]] 内置属性,所谓的原型链就是由它“链”起来的。
属性查找
当援用对象的属性时会触发 [[Get]] 操作,能够了解为会执行 [[Get]]()
,其逻辑是先查找以后对象是否存在该属性,如果存在就应用它。否则就去递归遍历,查找[[Prototype]] 属性所援用的对象中是否存在要查找的属性,如果找到则返回,否则直到[[Prototype]]=null 时查找完结,此时返回 undefined。
在应用 for in
遍历对象时原理和查找 [[Prototype]] 链相似,任何能够通过原型链拜访到并且是 enumerable
的属性都会被枚举。应用 in 操作符来查看属性在对象中是否存在时,会查找对象的整条原型链。
属性设置和屏蔽
上面以 myObject.foo = 'bar'
为例来阐明所有可能呈现的状况:
- 如果 myObject 本身存在 foo 属性,不论其 [[Prototype]] 链下层是否存在,都会产生屏蔽景象,会从新赋值。
- 如果 myObject 本身不存在,[[Prototype]]链上也不存在,则 foo 属性会被增加到 myObject 上。
-
如果 myObject 本身不存在,[[Prototype]]链上存在,会呈现如下三种状况:
- 如果 [[Prototype]] 链上的 foo 为一般数据拜访属性,并且没有被标记为只读,那就会在 myObject 上增加 foo 属性,它是屏蔽属性。
- 如果 [[Prototype]] 链上的 foo 被标记为只读,如果运行在严格模式下,会抛出一个谬误。否则,这条赋值语句被疏忽,并不会产生属性屏蔽。
- 如果 [[Prototype]] 链上的 foo 是一个 setter,那么肯定会调用这个 setter。foo 不会增加到 myObject,也不会从新定义 foo 这个 setter。
如果心愿在第二种和第三种下也屏蔽 foo,那就不能应用 =
操作符来赋值,而是应用 Object.defineProperty()
来向 myObject 增加 foo。
上面来个例子领会一下,同时为原型继承做一个铺垫
var person = {name: 'a'};
var fn = {getName: function() {return this.name;}
};
- 先想一下 person 为什么能够调用到 Object.prototype 中定义的办法?
Object 被定义为函数,上面会提到,只有是函数都会存在 prototype 属性,它指向一个对象,被称为原型对象,toString、hasOwnProperty 等办法就定义在该原型对象上。var person = {name: 'a'};
执行时会创立一个新的对象,并且底层会先将 person 的 [[Prototype]] 属性值设置为 Object.prototype,相当于执行 Object.setPrototypeOf({name: a}, Object.prototype)
。这样在整个[[Prototype]] 链上就能够找到这些办法了。
- 再想一下咱们怎么能使 person 对象能够调用 fn 中的 getName 办法呢?
- 应用 Object.assign(person, fn)将 fn 的 getName 间接增加到 person 对象中。
- 应用 Object.setPrototypeOf(person, fn),会将 person 中的 [[Prototype]] 属性由默认的 Object 改为指向 fn 对象,而 fn 中的 [[Prototype]] 会指向 Object.prototype。在执行 person.getName()时会进行属性查找,依据下面提到的规定,在其 [[Prototype]] 链上能够找到 getName 办法,其中的 this 利用的
隐式绑定
。
除此之外,还有另外一种实现形式,其原理和第二条一样:
var fn = {getName: function() {return this.name;}
};
// 创立一个新对象 person,使其 [[Prototype]] 指向 fn
var person = Object.create(fn);
person.name = 'a';
person.getName(); // 'a'
函数中的 prototype
JavaScript 中的函数有一种非凡个性:所有函数默认都会领有一个名为 prototype
的私有并且不可枚举的属性,他会指向一个对象:这个对象通常被称为该函数的原型。该函数同时存在内置属性[[Prototype]],留神这两者的区别!
同时该 prototype 对象存在一个叫 constructor
的属性,会持有该函数的援用。
这些个性与上面要说的“构造函数”没有任何关系,也就是说只有是函数就有这些个性。
构造函数
JavaScript 中把首字母大写的办法称为构造函数,这只是一种约定,同时这也意味着要应用 new 关键字来调用。
应用 new 调用函数会执行上面的步骤:
- 创立一个全新的对象。
- 这个新对象会被执行 [[Prototype]] 连贯。
- 这个新对象会被绑定到函数调用的 this。
- 如果函数没有返回其它对象。那么 new 表达式中的函数调用会主动返回这个对象。
上面是伪代码
function customNew(fn) {var o = {};
var rs = fn.apply(o, [].slice.call(arguments, 1));
Object.setPrototypeOf(o, fn.prototype);
return typeof rs === 'undefined' ? o : rs;
}
function Person(name) {this.name = name;}
Person.prototype.getName = function() {return this.name;};
var p = customNew(Person, 'JS');
p.getName(); // 'JS'
console.log(p.constructor === Person); // true
Person 的实例能够拜访到 getName
和constructor
都是基于“属性拜访”的原理。
继承
继承形式有多种,“红皮书”里也有讲到,最流行的一种是“组合继承”,即“借用构造函数”与“原型继承”组合起的一种形式。
function Foo(name) {this.name = name;}
Foo.prototype.myName = function() {return this.name;};
function Bar(name, label) {
// 借用 Foo 的构造函数
Foo.call(this, name);
this.label = label;
}
// Bar 的原型对象被赋值为一个全新的对象,// 该对象的 [[Prototype]] 指向 Foo.prototype 对象
Bar.prototype = Object.create(Foo.prototype);
// 设置不可枚举
Object.defineProperty(Bar.prototype, 'constructor', {
enumerable: false,
writable: true,
configurable: true,
value: Bar
});
// 或者能够用 ES6 中的 setPrototypeOf 办法,它只单纯批改 Bar.prototype 中的[[Prototype]],使之指向 Foo.prototype,并不会从新赋值,所以 constructor 不会失落
// Object.setPrototypeOf(Bar.prototype, Foo.prototype);
Bar.prototype.myLabel = function() {return this.label;};
var a = new Bar("name", "label");
console.log(a.myName()); // "name"
console.log(a.myLabel()); // "label"
console.log(Bar);
如果了解了属性查找及 prototype 和[[Prototype]],再来了解 Js 中的继承就容易多了。