你不知道的JS系列——全面解析[[Prototype]]、prototype、constructor

2,569 阅读7分钟

推翻观点

之前看一篇讲[[Prototype]]、prototypeconstructor的博文,文章强调两点:

  • __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]] 算法大致会检查下面这些内容:

  1. 属性是否是访问描述符?如果是并且存在 setter 就调用 setter。
  2. 属性的数据描述符中 writable 是否是 false ?如果是,在非严格模式下静默失败,在 严格模式下抛出 TypeError 异常。
  3. 如果都不是,将该值设置为属性的值。

好了,上面都是你该知道的,那接下来讲些你不知道的。以obj.a = 'welcome'赋值操作展开讲解,左半部分obj.a进行[[Get]]算法查找,如果在obj对象上找到了a属性,那么赋值语句就是简单的修改a属性的值。如果a属性既不存在于对象自身,也不存在于[[Prototype]]链上,那么a属性会被直接添加到obj上。这里的重点就在于a属性存在于[[Prototype]]链上时,会发生下面三种情况:

  1. 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
  1. 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"
  1. a的访问描述符设置了setter,那一定会调用setter,不会创建屏蔽属性,也不会重新定义asetter
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,所以执行getterObject.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的。

Objectprototype

Object.prototype上面有__proto__属性的访问描述符setget,如果你再试图获取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__()

Functionprototype

毫无疑问,必有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为构造函数,person1person2为实例对象。

结合上图,很容易理解:Person.prototype.constructor === Person。在new的过程中会调用构造函数,所以本质上就是调用的函数本身。