本文主要总结了大神的专栏,如果有不对之处,欢迎指正。
原型(Prototype)模式是一种设计模式,同时更是一种编程范式(programming paradigm)
理解原型编程范式
类是先抽象后具体,也就是必须先有类,之后才是实例,然后在关注具体层面的东西。比如 JAVA 中,类是它面向对象系统的根本。
原型是先具体后抽象,将相似实例关联到一个原型对象,从而囊括较为通用的行为和属性。基于此原型,通过 “复制” 来创建新对象。
JS 中,原型是面向对象系 统的根本。
JS 中怎么实现复制的呢?
JS 是通过使新对象保持对原型对象的引用来做到了 “复制”(就是新对象上面有 proto)。
JS 不是有 Class 么?
注意,JS 的 class 只是原型的语法糖:
class Dog {
constructor(name, age) {
this.name = name;
this.age = age;
}
eat() {
console.log("肉骨头真好吃");
}
}
等价于:
function Dog(name, age) {
this.name = name;
this.age = age;
}
Dog.prototype.eat = function() {
console.log("肉骨头真好吃");
};
理解原型与原型链
原型编程范式的核心思想就是利用实例来描述对象,用实例作为定义对象和继承的基础。
JS 里,原型编程范式的体现就是基于原型链的继承,每个新对象都保留了对原型对象的引用。
- 每个构造函数都拥有一个
prototype属性,它指向构造函数的原型对象,这个原型对象中有一个construtor属性指回构造函数 - 每个实例都有一个proto属性,当我们使用构造函数去创建实例时,实例的proto 属性就会指向构造函数的原型对象。
用例子来理解原型链:
// 创建一个Dog构造函数
function Dog(name, age) {
this.name = name;
this.age = age;
}
Dog.prototype.eat = function() {
console.log("肉骨头真好吃");
};
// 使用Dog构造函数创建dog实例
const dog = new Dog('not yan', 1)
// 输出"肉骨头真好吃"
dog.eat();
// 输出"[object Object]"
dog.toString();
dog 实例里并没有 eat 方法和 toString 方法,它们还是被成功地调用了。
因为当读取一个实例的属性/方法时,它首先搜索这个实例本身,若没找到,会向上去搜索实例的原型对象;
若还没找到,它就去搜索原型对象的原型对象,一直到Object.prototype为止,这个搜索的轨迹,就叫做 原型链。
function A() {
this.name = "a";
this.color = ["green", "yellow"];
}
function B() {}
B.prototype = new A();
var b1 = new B();
var b2 = new B();
// 此处是赋值,并不是读取,所以不会往上找
b1.name = "change";
// b1.color是读取,如果b1上面没有,是会往上找的
b1.color.push("black");
console.log(b2.name); // 'a'
console.log(b2.color); // ["green", "yellow", "black"]
new 做了啥
实例大多是被 new 出来的。
那么 new 到底做了什么呢?
- 为这个新的对象开辟一块属于它的内存空间
- 把函数体内的 this 指到 开辟的内存空间去
- 将新对象的
__proto__这个属性指向对应构造函数的prototype属性的引用,把实例和原型对象关联起来。注意这里是引用。 - 执行函数体内的逻辑,有可能创建新对象的属性和方法
- 最后即便你没有手动 return,构造函数也会帮你把创建的这个新对象 return 出来
var A = function() {};
A.prototype.n = 1;
var b = new A();
A.prototype = {
n: 2,
m: 3
};
var c = new A();
console.log(b.n);
console.log(b.m);
console.log(c.n);
console.log(c.m);
答案是1 undefined 2 3。
注意,b 实例创建的时候,构造函数的 prototype 的引用是{n:1,constructor:A},所以 b 实例的proto也是指向{n:1,constructor:A}
之后进行A.prototype ={...},切断了和旧 prototype 的关系,
而 b 却仍然保留着旧 prototype 的引用。这就是造 成 b 实例和 c 实例之间表现差异的原因。
再看个例子
function A() {}
function B(a) {
this.a = a;
}
function C(a) {
if (a) {
this.a = a;
}
}
A.prototype.a = 1;
B.prototype.a = 1;
C.prototype.a = 1;
console.log(new A().a); // 1
console.log(new B().a); // undefined
console.log(new C(2).a); // 2
这里主要注意,new操作里,执行函数体的时候,实例会新增自己的属性和方法。
手写实现 new
回忆new 做的事:为新对象开辟内存空间、新对象的 __proto__ 指向构造函数的原型对象、函数体的this 指向新对象、执行函数体、函数体没有返回新对象则主动返回新对象
function myNew(FnCreator, ...args) {
// 为新对象开辟内存空间
let newObj = {};
// 新对象的__proto__指向构造函数的原型对象
newObj.__proto__ = FnCreator.prototype;
// this指向新对象、执行函数体
const res = FnCreator.call(newObj, ...args);
// 函数体没有返回新对象则主动返回新对象
return typeof res === "object" ? res : newObj;
}
// 创建一个Dog构造函数
function Dog(name, age) {
this.name = name;
this.age = age;
}
Dog.prototype.eat = function() {
console.log("肉骨头真好吃");
};
// 创建dog实例
var dog = myNew(Dog,'d',2)
console.log(dog)
// 输出"肉骨头真好吃"
dog.eat();
// 输出"[object Object]"
dog.toString();