JavaScript prototype

914 阅读4分钟

本文主要对JS原型相关的问题做一些总结。从一个简单的例子说起:

function Foo() {
    ...
}

Foo.prototype.constructor === Foo; // true

let foo = new Foo();
foo.constructor === Foo; // true

当声明一个函数的时候,默认会给函数的prototype添加一个.constructor属性,该属性指向该函数。当调用foo.constructor的时候可以看到通用指向Foo函数,是不是foo实例也同样有constructor属性?

答案是否定的!这里foo是通过原型找到该属性的,即foo.__proto__.constructor。那foo.__proto__又是指向哪里呢?

其实foo.__proto__就是指向了Foo.prototypefoo.__proto__ === Foo.prototype

我们知道,在对象的原型上添加方法,则实例可以通过原型找到对应的方法,譬如:

Foo.prototype.sayHello = function() {
    return "hello";
}

foo.sayHello(); // hello

这里foo通过__proto__.sayHello, 将方法调用委托给了Foo.prototype对象,调用Foo.prototype对象上的sayHello方法。同样之前将到的foo.constructor也是委托给了Foo.prototype对象。

如果我们手动修改了Foo.prototype对象, 如:

function Foo() {
    ...
}

Foo.prototype = {
    // 重置该对象为空后没有constructor属性
}

let foo = new Foo();
foo.constructor === Foo; // false

此时foo.constructor时,即foo.__proto__是指向了新的Foo.prototype={}的,自然是找不到constructor属性了!说明无意间对prototype的修改会导致原型链的断裂。

1. __proto__ 与 prototype

__proto__prototype都是维护原型链重要的部分,但这两种有什么区别呢?

  • __proto__ : JS每个对象内部都隐含了一个__proto__属性,用来指向它的原型对象,即xxx.prototype。例如对象a:let a = {name: "example"}

    object-a-example

  • prototype : 与__proto__不同的是,这个属性只有函数才有。并且添加到prototype上的属性和方法在所有实例中都是共享的。譬如Foo.prototype,可以看到Foo.prototype中也有__proto__属性,指向它的原型对象。

    object-a-example

2. 实现继承

通过原型很容易实现对象间的继承,JS中继承是通过委托机制实现的。最常用的一种方式是通过Object.create(..)方法。

function Foo() {
    this.name = "Foo";
}

Foo.prototype.sayName = function () {
    return "This is " + this.name;
}

function Bar() {
    this.name = "Bar";
}

// 让Bar继承Foo
Bar.prototype = Object.create(Foo.prototype);

let bar = new Bar();
bar.sayName();  // This is Bar

通过Object.create(..)方法,让Bar的原型链指向Foo的原型实现继承效果。如图所示:

Bar-Foo 继承图

为了详细了解继承效果,我们也可以自己动手实现。自己实现一个生成子类的方法subClassOf()

        function subClassOf(o) {
            function F() {};
            F.prototype = o;
            return new F();  // new F() 产生一个新对象,该对象内部__proto__关联F.prototype.
        }

        Bar.prototype = subClassOf(Foo.prototype);

        let bar = new Bar();
        bar.sayName(); // This is Bar

原理其实很简单,即通过一个中间函数F作为中间桥梁,连接BarFoo的原型链,如下图所示:

Bar-F-Foo

这里注意的是,如果写成Bar.prototype = subClassof(Foo)这样的话,构造的原型链是不成立的,如图:

Bar-F-Foo-error

这里F.prototype = Foo,并没有指向Foo.prototype, 导致原型链断裂,当调用bar.sayName()时,会报找不到对应方法,因为该方法是在Foo.prototype上的。

JS Object Layout

这张图是该篇博文中的一张JS原型关系图,这张图很清楚的显示了原型中的各种关系。

js-object-layout

通过这副图可以总结以下几个要点:

  • Foo()函数和其实例f1都可以访问到Foo.prototype。因此两者都可以访问原型链上的方法。
  • Foo.prototypeconstructor是指向自身函数Foo()的。constructor存在的意义是为了让实例对象f1可以借助constructor访问定义在Foo()函数中的属性和方法。

注意以下这种情况:

Bar.prototype = new Foo();

这种情况下Bar.prototype.constructor的指向是错误的,此时是指向的Foo函数,所以必须要修正constructorBar.prototype.constructor = Bar

3. 原型链获取

获取一个对象的原型链,可以直接通过__proto__获取,如 bar.__proto__。但这是一种非标准的方法,并不是所有浏览器都支持。ES5中的标准方法是Object.getPrototypeOf()

Bar.prototype = new Foo();

// 修正前
Bar.prototype.constructor === Bar; // false;

// 修正后
Bar.prototype.constructor = Bar;
Bar.prototype.constructor === Bar; // true;
Object.getPrototypeOf(bar) === bar.__proto__ === Bar.prototype

4. 对象关联

如果要使两个对象关联起来,比如让对象bar关联对象foo, 同样使用Object.create()方法

        function Foo() {
            this.name = "Foo";
            this.something = function() {
                console.log("This is something");
            }
        }

        Foo.prototype.sayName = function () {
            console.log("This is " + this.name);
        }
        
        // bar 关联 foo
        let bar = Object.create(foo);

这样bar可以调用foosomething()方法和sayName()方法。

这里需要注意的是,bar对象直接关联到foo对象,与上述讲的继承关系又不一样了,继承时是通过Bar.prototype = Object.create(Foo.prototype)将原型链进行关联的。可以看到:

Object.getPrototypeOf(bar) === Bar.prototype; // false
Object.getPrototypeOf(bar) === foo; // true

这里仅仅是两个对象的关联!因此不仅可以调用Foo.prototype原型上的方法,也可以调用foo对象上的方法。如图:

bar-foo

参考

1.Javascript Object Hierarchy

2.你不知道的Javascript 上卷