JavaScript之原型
Posted seven
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript之原型相关的知识,希望对你有一定的参考价值。
[[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中的继承就容易多了。
以上是关于JavaScript之原型的主要内容,如果未能解决你的问题,请参考以下文章
JavaScript之面向对象学九(原型式继承和寄生式继承)
JavaScript之面向对象学习五(JS原生引用类型ArrayObjectString等等)的原型对象介绍