理解原型设计模式以及 JavaScript 中的原型规则

我们创建的每个函数都有一个 protoype 属性,这个 prototype 属性是一个指针,指向原型对象

这个对象的用途:包含由特定类型的所有实例共享的属性和方法(通俗的说就是,所有特定类型的实例都可以共享这个对象上的属性和方法)。

原型对象的两种赋值方法

给原型对象添加属性或方法

1
function Person() {}
2
// Person 原型对象上默认只有一个 constructor 属性
3
// 这个 constructor 指向 Person
4
Person.prototype.name = "jser";
5
Person.prototype.age = 16;
6
Person.prototype.sayName = function() {
7
    return this.name;
8
}
9
10
let per1 = new Person();
11
let per2 = new Person();
12
per1.sayName === per2.sayName; // true
13
per1.age === per2.age;  // true

正如上边所说,实例 per1per2 共享 Person 原型对象的属性和方法。

重写原型对象

1
function Person() {}
2
Person.prototype = {
3
    name: "jser",
4
    age: 16,
5
    sayName() {
6
        return this.name;
7
    }
8
}
9
let per1 = new Person();
10
let per2 = new Person();
11
per1.sayName === per2.sayName; // true
12
per1.age === per2.age;  // true

这种方式会重写原型对象,重写后的原型对象默认的 constructor 属性不再指向 Person 此时它指向 Object。(下文会有讲到)

我们可以手动将它指向 Person

1
Person.prototype = {
2
    constructor: Person,
3
    ...
4
}

原型对象内存分析

屏幕快照 2019-09-25 下午4.25.37.png

默认情况下,原型对象 Person.prototype 会包含一个 constructor 属性,这个属性上也有一个 prototype 属性,这个属性是一个指针,指向原型对象

1
创建的实例也有 constructor 属性,可以使用 实例.constructor.prototype 去修改或扩展原型对象。
1
Person.prototype.constructor === Person // true
2
Person.prototype.constructor.prototype === Person.prototype // true

当调用构造函数创建一个新实例后,该实例内部会包含一个内部属性 __proto__,它指向构造函数的原型对象。

1
person1.__proto__ === Person.prototype // true

这个连接只存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间,也就是说这个内部属性和构造函数没有直接的关系。

原型对象的值不能被实例重写

1
function Person() {}
2
Person.prototype = {
3
    constructor: Person,
4
    name: "jser",
5
    age: 16,
6
    sayName() {
7
        return this.name;
8
    }
9
}
10
let per1 = new Person();
11
let per2 = new Person();
12
per1.name = "zxl"; // 重写 name
13
console.log(per1.name);  // zxl person实例 在自身查到到了 name 
14
console.log(per2.name);  // jser 在原型对象上查找到的 name

可以看到原型对象中的 name 没有被改变,person2.name 在原型对象上查找的值仍为 "jser"

原型模式查找属性原则:

  • 原型模式中,当通过实例读取属性值时,首先会在实例自身上查找,如果实例上没有,则就会去原型对象上搜索;

  • 如果在自身上找到,就使用这个值,不会再继续去原型对象上查找。

屏幕快照 2019-09-25 下午5.13.55.png

原型模式的动态性

上文说到不能通过实例对象来重写原型对象上的属性,如果想要重写原型对象上的属性,只能通过原型对象自身去修改。

1
...
2
Person.prototype.age = 22;

原型模型原则:由于在原型中查找值的过程是一次搜索,因此在对原型对象做的任何修改都会立即从实例上反映出来,即使是先创建实例后修改原型也如此。

1
// 先创建实例 后修改原型对象
2
let per2 = new Person();
3
Person.prototype.age = 22;
4
console.log(per2.age); // 变成改写后的 22
5
6
// 后创建实例 先修改原型对象
7
Person.prototype.age = 22;
8
let per2 = new Person();
9
console.log(per2.age); // 变成改写后的 22

如果先创建了实例,之后用字面量赋值的方式来重写原型对象,这就会切断现有原型对象与之前存在的任何实例之间的联系。

1
function Person() {}
2
let per1 = new Person();
3
Person.prototype = {
4
    name: "jser",
5
    age: 16,
6
    sayHi() {
7
        console.log("Hi");
8
    }
9
}
10
per1.sayHi();

这段代码会报 Uncaught TypeError: per1.sayHi is not a function,说明实例 per1 没有搜索不到 sayHi 这个方法,因为在创建 per1 实例之前,per1.__proto__ 指向的原型对象只有一个默认属性 constructor

1
{ constructor: ƒ }

重写的 Person.prototype 被分配在了新的内存空间中:

屏幕快照 2019-09-25 下午5.58.18.png

原型对象的缺点

原型对象的好处是原型中的所有属性和方法可以被很多实例共享。

缺点是当原型中包含引用类型的值的属性时,一个实例对象对这个引用类型的属性做了修改,在其他实例对象中也可以体现出来。

1
function Person() {}
2
Person.prototype = {
3
    constructor: Person,
4
    name: "jser",
5
    age: 16,
6
    friends: ["javaer", "phper"]
7
}
8
let per1 = new Person();
9
per1.friends.push("pythener");
10
let per2 = new Person();
11
console.log(per2.friends)

原型对象中引用类型属性 friends 被修改成了 ["javaer", "phper", "pythener"],因此实例对象 per2 搜索到的结果也被 per1 修改了。

总结

从原型对象中体现的一些原型模式的规则可以总结如下

  • 构造函数有一个 prototype 指针指向原型对象,构造函数的实例共享原型对象上的属性和方法。

  • 实例与原型对象之间有一个 __proto__ 连接。

  • 原型对象上有一个 constructor 属性默认指向构造函数,constructor 属性上有一个 prototype 指针指向原型对象。

  • 原型对象上的值不能被实例重写。

  • 在原型模式中,通过实例子搜索属性时,实例上的属性会屏蔽原型对象上的同名属性。

  • 原型模式中查找值的过程是一次搜索,因此在对原型对象做的任何修改都会立即从实例上反映出来,即使是先创建实例后修改原型也如此。

  • 如果先创建了实例,之后用字面量赋值的方式来重写原型对象,这就会切断现有原型对象与之前存在的任何实例之间的联系。

  • 原型可以被继承,如通过 Object.create(prototype, optionalDescriptorObjects) 来实现原型继承。

拓展

instanceof 的底层实现原理

instanceof是用来判断 a 是否为 B 的实例,表达式为:a instanceof B,如果 aB 的实例,则返回 true,否则返回 false

在这里需要特别注意的是:instanceof 检测的是原型对象。

1
// 底层实现原理:
2
instanceof (a,B) = {
3
    var L = a.__proto__;
4
    var R = B.prototype;
5
    if(L === R) {
6
        // A的内部属性 __proto__ 指向 B 的原型对象
7
        return true;
8
    }
9
    return false;
10
}

代码的基本实现:

1
function _instanceof(instance, F) {
2
    if(!F) `Right-hand side of 'instanceof' is not an object`;
3
    const L = instance.__proto__;
4
    const R = F.prototype;
5
    if(!L) throw `Unexpected identifier`;
6
    if(instance.__proto__ === F.prototype) {
7
        return true;
8
    }
9
    return false;
10
}

描述 new 一个对象的详细过程,手动实现一个 new 操作符

使用 new 操作符来调用函数时,会自动执行下面的操作:

1.创建一个新的空对象;

2.这个对象会被执行 [[prototpye]] 连接;

3.这个新对象会绑定到函数调用的 this

4.如果函数没用返回其他对象,那么 new 调用的函数会自动返回这个新对象。

手动实现:

1
// new 操作符的模拟实现
2
function _new() {
3
    let o = {}; // 创建一个空对象
4
    let C = arguments[0];  // 获得构造函数
5
    o.constructor = C;     // 将空对象的 constructor 默认指向构造函数
6
    o.__proto___ = C.prototype;  // 进行 [[prototype]] 连接
7
    let res = C.apply(o, arguments);  // 进行 this 绑定 并获得构造函数返回值
8
    // 返回处理
9
    return typeof res === "object" ? res : o;
10
}
11
// 测试
12
function Person() {
13
    this.name = "jser"
14
}
15
var p = _new(Person);
16
p.name; // ”jser“
17
p.constructor  // Person

理解 es6 中 class 构造以及继承的底层实现原理