推翻观点
之前看一篇讲[[Prototype]]、prototype与constructor的博文,文章强调两点:
__proto__(就是将要讲的[[Prototype]])和constructor属性是对象所独有的prototype属性是函数所独有的
对于初学者来看,并无不妥,拍手称赞。本着负责的态度,告诉你事实是:
[[Prototype]]虽然显示是对象的属性,但它本质上是Object.prototype对象上的属性prototype属性是函数对象所独有的,constructor属性是原型对象所独有的
如果你有疑问,且听细细道来。
说明
这篇文章是给下篇文章深入继承做铺垫的,当然搞懂这些概念本身就很重要。在讲关于原型的这些复杂概念前,我们先讲点别的,就讲[[Get]]和[[Put]],别担心,讲这些当然有用,耐住性子。
[[Get]]
所有的对象其实都内置了[[Get]],它是一种定义好的算法,用于在对象中按指定规则查找指定的属性。以最简单的获取对象属性来说:
var obj = {
a: 1
}
obj.a; // 1
这里obj.a就是执行了一次属性访问,但这条语句并不像你看起来的那么简单。执行操作,首先obj对象会调用内置的[[Get]]算法,在自身查找是否具有名称相同的属性(本例查找到了),如果在自身未查找到,那就进行自身原型链上的查找。如下:
var obj = {
a: 1
}
var myObj = Object.create(obj);
myObj.a; // 1
好的,我们在myObj的[[Prototype]](大家喜欢叫原型)上找到了属性a,如果没有找到,那这个查找过程会一直在[[Prototype]]链上持续下去,直到找到或查找到[[Prototype]]链的尽头,这个尽头就是Object.prototype。如果在尽头也没找到,那就返回undefined。
Tips:为什么尽头会是Object.prototype?
正如大家所知道的那样,
var a = {}是等价于var a = new Object()的,在《你不知道的JS系列——全面解析this》篇章中讲过,new调用构造函数的过程中,第2步会执行 [[ 原型 ]] 连接,对应到这里就是将a对象的[[Prototype]]连接到(指向)Object.prototype,所以所有普通对象的[[Prototype]]链顶端都会是Object.prototype。
[[Put]]
既然有可以获取属性值的 [[Get]] 操作,根据相对论(哈哈),那就一定有对应的 [[Put]] 操作。
[[Put]]也是一种定义好的算法,在给对象的属性赋值时,触发[[Put]]。如果对象已经存在这个属性,[[Put]] 算法大致会检查下面这些内容:
- 属性是否是访问描述符?如果是并且存在 setter 就调用 setter。
- 属性的数据描述符中 writable 是否是 false ?如果是,在非严格模式下静默失败,在 严格模式下抛出 TypeError 异常。
- 如果都不是,将该值设置为属性的值。
好了,上面都是你该知道的,那接下来讲些你不知道的。以obj.a = 'welcome'赋值操作展开讲解,左半部分obj.a进行[[Get]]算法查找,如果在obj对象上找到了a属性,那么赋值语句就是简单的修改a属性的值。如果a属性既不存在于对象自身,也不存在于[[Prototype]]链上,那么a属性会被直接添加到obj上。这里的重点就在于a属性存在于[[Prototype]]链上时,会发生下面三种情况:
a的数据描述符为正常的writable: true,那就会直接在obj中添加一个名为a的新 属性,它屏蔽掉了[[Prototype]]链上的属性a,所以我们叫它屏蔽属性。
var proto = {};
Object.defineProperty(proto, 'a', {
writable: true,
value: 'thank',
configurable: true,
enumerable: true
})
var obj = Object.create(proto);
obj.hasOwnProperty('a'); // false
obj.a = 'welcome';
obj.hasOwnProperty('a'); // true
a的数据描述符为writable: false,那么无法修改已有属性a或者在obj上创建屏蔽属性。严格模式下会报错。
var proto = {};
Object.defineProperty(proto, 'a', {
writable: false,
value: 'thank',
configurable: true,
enumerable: true
})
var obj = Object.create(proto);
obj.hasOwnProperty('a'); // false
obj.a = 'welcome';
obj.hasOwnProperty('a'); // false
obj.a; // "thank"
a的访问描述符设置了setter,那一定会调用setter,不会创建屏蔽属性,也不会重新定义a的setter。
var proto = {};
Object.defineProperty(proto, 'a', {
configurable: true,
enumerable: true,
get: function(){
return this.__a__;
},
set: function(val){
this.__a__ = "thank";
}
})
var obj = Object.create(proto);
obj.hasOwnProperty('a'); // false
obj.a = 'welcome';
obj.hasOwnProperty('a'); // false
obj.a; // "thank"
[[Prototype]]
[[Prototype]]是什么?
JavaScript 中的对象有一个特殊的 [[Prototype]] 内置属性,其实就是对于其他对象的引 用。几乎所有的对象在创建时 [[Prototype]] 属性都会被赋予一个非空的值。
[[Prototype]]有什么用?
在个人看来,更多的是为了继承引出来的这个概念,简单来说在JavaScript中将两个对象的[[Prototype]]指向另外同一个[[Prototype]],即达到继承(这里是原型继承),大家都会有公共的部分。关于继承,在下一篇文章讲深入继承时会详细介绍。
[[Prototype]]在浏览器的实现__proto__
上面讲对象都有一个特殊的 [[Prototype]] 内置属性,在浏览器的中用__proto__表示,注意不要把__proto__当做标准来使用,更多的时候我们应该使用Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。
在这里我要强调的是__proto__与prototype虽然指向的都是对象,但它们不是一个东西,否则也不会起两个名字这么麻烦。
__proto__每一个对象都有,函数是特殊的对象,自然也有prototype只有函数独有
截图为证:
__proto__默认指向构造函数的prototype
var obj = new Object(); // 我们一般写 var obj = {};
obj.__proto__ === Object.prototype; //true
上面代码的构造函数是Object,所以__proto__指向了Object.prototype。
再看一个例子:
var fun = new Function(); // 我们一般写 var fun = function(){};
fun.__proto__ === Function.prototype; // ture
插点题外话:
typeof function(){} // "function"
typeof {} //"object"
引用类型只有Object,函数也是对象,这里返回function,意在表明这是一个函数类型的对象,便于区分,不然显得太繁杂了。所以函数对象本来就特殊,再来看,只有函数独有prototype,就不显得那么突兀了。
__proto__存在于Object.prototype
直接上代码证明:
var a = {};
a.hasOwnProperty('__proto__'); // false
Object.prototype.hasOwnProperty('__proto__'); // true
再看看上面的截图,一脸懵逼,这对不上啊。其实__proto__ 的实现大致上是这样的:
Object.defineProperty( Object.prototype, "__proto__", {
get: function() {
return Object.getPrototypeOf( this );
},
set: function(o) {
Object.setPrototypeOf( this, o );
return o;
}
})
这里只看getter,比如现在执行a.__proto__,在a的[[Prototype]]链终端Object.prototype上找到属性__proto__,由于定义了getter,所以执行getter,Object.getPrototypeOf( this )这里的this指向对象a(隐式绑定,a调用的__proto__),再看下Object.getPrototypeOf()在MDN上的定义:
Object.getPrototypeOf() 方法返回指定对象的原型(内部[[Prototype]]属性的值)。
所以周周转转,返回的还是a的__proto__,这就解释了为什么__proto__是存在于Object.prototype的。
prototype
prototype是什么?
函数默认的 prototype属性其实也是一个普通的对象,它上面有一个constructor属性,由于所有对象都内置[[Prototype]]属性,所以还有一个属性__proto__。这里的__proto__当然也是指向Object.prototype的。
Object的prototype
Object.prototype上面有__proto__属性的访问描述符set、get,如果你再试图获取Object.prototype.__proto__,根据上面__proto__的实现,那么返回Object.getPrototypeOf( Object.prototype ),自身调用自身的方法,无疑陷入死循环,所以规定Object.prototype.__proto__ === null
typeof Object.prototype // "object"
Object.prototype // 下面是具体内容
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toLocaleString: ƒ toLocaleString()
toString: ƒ toString()
valueOf: ƒ valueOf()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()
Function的prototype
毫无疑问,必有constructor与__proto__,当然还有函数自身定义的一些属性,比如apply、bind、call等
console.dir(Function.prototype) //下面是具体内容
{
apply: ƒ apply()
arguments: (...)
bind: ƒ bind()
call: ƒ call()
caller: (...)
constructor: ƒ Function()
length: 0
name: ""
toString: ƒ toString()
Symbol(Symbol.hasInstance): ƒ [Symbol.hasInstance]()
get arguments: ƒ ()
set arguments: ƒ ()
get caller: ƒ ()
set caller: ƒ ()
__proto__: Object
[[FunctionLocation]]: <unknown>
[[Scopes]]: Scopes[0]
}
然后再看Function.prototype.__proto__,毋庸置疑,还是指向的Object.prototype
Function.prototype.__proto__ === Object.prototype; // true
constructor
constructor是位于原型对象上的一个属性,看下面经典图:
Person为构造函数,person1、person2为实例对象。
结合上图,很容易理解:Person.prototype.constructor === Person。在new的过程中会调用构造函数,所以本质上就是调用的函数本身。