推翻观点
之前看一篇讲[[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
的过程中会调用构造函数,所以本质上就是调用的函数本身。