一、原型和原型链
-
原型本质上是一个对象,存在于每个非箭头函数中,所以每个非箭头函数都有一个属性 prototype 指向原型对象
-
原型对象,构造函数,实例对象三者之间的关系
- 构造函数中
prototype属性指向原型对象 - 原型对象中
constructor属性指向构造函数 - 实例对象中
__proto__属性指向原型对象
- 构造函数中
- 非空类型数据,都具有对象,因为从本质上他们都是通过对应构造函数构建出来的,所以他们都具有__proto__属性,指向构造函数的原型对象
- 要判断某个值其原型对象,只需要确认该值是通过哪个构造函数构建的即可,只要确认了构造函数,那么该值的__proto__必然指向该构造函数的prototype
- 原型链: 根据上下文,所有非空数据,都可以通过__proto__指向原型对象,同时如果原型对象非空,那么必然同样会有__proto__指向他自己的原型对象,如此一层层往上追溯,以此类推,就形成了一整条链路,一直到某个原型对象为null,才到达这条链路的最后环节,原型对象之间这种链路关系被称之为原型链。
- 原型链最后都会到Object.prototype ,因为原型对象,本质上就是一个对象,由Object 进行创建,其__proto__指向object.prototype,同时约定
Object.prototype.__proto__等于null所有原型链的重点都已结束。 - 作用:
- 实现继承:js中继承主要就是通过原型、原型链来实现的
- 为某一类型数据设置共享属性,方法,将大大节约内存
- 查找属性:当我们试图访问对象属性时,它会先在当前对象上进行查找,没有查找到就会继续查找该对象的原型对象,以及该对象的原型对象的原型对象,依此向上查找,直到找到一个名字匹配的属性或者到达原型链的重点
__proto__并不是ECMAScript 语法规范的标准,他只是大部分浏览器厂商实现或者说支持的一个属性,通过该属性方便我们访问、修改原型对象,从 ECMAScript 6 开始,可通过 Object.getPrototypeOf() 和 Object.setPrototyprOf() 来访问、修改原型对象
二、常用继承方案
2.1 原型链继承( 重写prototype )
- 实现:直接重写构造函数的原型,将构造函数的原型赋值为想要继承的父级构造函数的实例对象
- 缺点:通过实例对象改变某个key值,所有实例相应的值都发生改变:
原型如果包含引用值, 修改引用值所有实例都会改动到;子类在实例化时不能给父类的构造函数传参 - 案例:
function Parent() {
this.name = 'parent';
this.play = [1, 2, 3]
}
function Child() {
this.type = 'child';
}
// 重写Child的原型对象
Child.prototype = new Parent();
// Parent实例上没有constructor,需要手动挂上构造器,指向自己的构造函数
Child.prototype.constructor = Child;
var child1 = new Child();
var child2 = new Child();
// 改变Child某个实例对象key(Child原型上的)
child1.play.push(4);
// 所有实例对象上的相应的key值都发生了变化
console.log(child1.play); // [1, 2, 3, 4]
console.log(child2.play); // [1, 2, 3, 4]
console.log(new Child().play); // [1, 2, 3, 4]
console.log(new Parent().play); // [1, 2, 3]
2.2 构造函数继承( Parent.call(this) )
- 实现:在构造函数内将父级构造函数的this指向当前构造函数的this
- 缺点: 只能继承父级实例上的属性和方法,不能继承父级原型对象(prototype)上的属性和方法;
instanceof操作符和isPrototypeOf()方法无法识别出合成对象继承于哪个父类。 - 优点: 可解决上文提到的
引用值问题, 每个实例都是新建一个引用值;支持为父类构造函数传参 - 案例:
function Parent(){
this.name = 'parent';
}
Parent.prototype.getName = function () {
return this.name;
}
function Child(){
Parent.call(this);
this.type = 'child'
}
let child = new Child();
console.log(child); // 没问题
console.log(child.getName()); // 会报错 Uncaught TypeError: child.getName is not a function
2.3组合继承( 重写prototype + Parent.call(this) )
实现:重写构造函数的原型为父级构造函数的实例对象,同时在构造函数内将父级构造函数的this指向当前的构造函数 缺点:Parent会被调用两次 例子:
function Parent () {
this.name = 'parent';
this.play = [1, 2, 3];
}
Parent.prototype.getName = function () {
return this.name;
}
function Child() {
// 第二次调用 Parent()
Parent.call(this);
this.type = 'child';
}
// 第一次调用 Parent()
Child.prototype = new Parent();
// 手动挂上构造器,指向自己的构造函数
Child.prototype.constructor = Child;
console.log(new Child());
var child1 = new Child();
var child2 = new Child();
child1.play.push(4);
console.log(child1.play); // [1, 2, 3, 4]
console.log(child2.play); // [1, 2, 3]
console.log('child1 getName...', child1.getName()); // 正常输出'parent'
console.log('child2 getName...', child2.getName()); // 正常输出'parent'
2.4原型式继承( Object.create(Parent) )
实现:利用Object.create方法实现普通对象的继承 缺点: 原型指向同一个对象,通过某个实力改变的原型上的key,会导致所有实例读取到的值都是被修改后的key值(object.create方法实现的是浅拷贝,多个实例的引用类型属性执行相同的内存) 例子:
let parent = {
name: "parent",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
// 声明一个过渡对象
function clone(original) {
let clone = Object.create(original);
return clone;
}
let person1 = clone(parent);
person1.name = "tom";
person1.friends.push("jerry");
let person2 = clone(parent);
person2.friends.push("lucy");
console.log(person1.name); // tom
console.log(person1.name === person1.getName()); // true
console.log(person2.name); // parent
console.log(person1.friends); // ["p1", "p2", "p3","jerry","lucy"]
console.log(person2.friends); // ["p1", "p2", "p3","jerry","lucy"]
2.5寄生式继承 ( Object.create(Parent) ),添加额外的属性和方法
跟原型式继承一样的,就是多加了一些额外的属性和方法,缺点也一样 实现:利用Object.create 方法实现普通对象的继承,并添加额外的属性和方法 缺点: 原型指向同一个对象,通过某个实例改变的原型上的key ,会导致所有实例读取道德值都是被修改后的key 值(Object.create 方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存)
let parent = {
name: "parent",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
// 声明一个过渡对象
function clone(original) {
let clone = Object.create(original);
clone.getFriends = function() {
return this.friends;
};
return clone;
}
let person1 = clone(parent);
person1.name = "tom";
person1.friends.push("jerry");
let person2 = clone(parent);
person2.friends.push("lucy");
console.log(person1.name); // tom
console.log(person1.name === person1.getName()); // true
console.log(person2.name); // parent
console.log(person1.friends); // ["p1", "p2", "p3","jerry","lucy"]
console.log(person2.friends); // ["p1", "p2", "p3","jerry","lucy"]
2.6 寄生组合式继承 ( Object.create(Parent)+ Parent.call(this) )
实现:重写构造函数的原型为父级构造函数的实例对象,同时在构造函数内将父级构造函数的this指向当前的构造函数 优点:Parent 指挥调用1次,是比较优选的继承方法
function Parent() {
console.log('Parent....');
this.name = 'parent';
this.play = [1, 2, 3]
}
function Child() {
// 2、在构造函数内部将父级构造函数this指向当前函数的this
Parent.call(this);
this.type = 'child';
this.friends = 'lucy';
}
// 1、重写原型对象,还原构造器
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
Child.prototype.getFriends = function () {
return this.friends;
}
var child = new Child();
var child1 = new Child();
child1.play.push(44);
console.log('child play...', child.play);// child play... (3) [1, 2, 3]
console.log('child1 play...', child1.play);// child1 play... (4) [1, 2, 3, 44]
2.7 ES6 类的继承 ( extends + supper(props) )
利用es6 的 class ,结合 extends 关键字和 supper(props) 方法实现对父类的属性和方法的继承 例子:
class Person {
constructor(money) {
this.money = money;
}
getMoney() {
console.log(this.name + " get Person's money $" + this.money)
}
}
class Child extends Person {
constructor(money, name) {
// 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
super(money);
this.name = name;
}
}
const child = new Child(1000, 'child');
child.getMoney();// child get Person's money $1000
三、 New 操作符做了哪些事情
- 创建一个新的空对象 A
- 往空对象挂载 构造函数 Con 的原型对象: 对象 A 创建
__proto__属性,并将构造函数的prototype 属性赋值给__proto__ - 执行构造函数 con :改变构造函数 this 指向,指向空对象 A,并执行 构造函数,往空对象注入属性
- 判断构造函数是否返回一个对象?
- 是:如果构造函数也返回了一个对象 B ,则最终 new 出来的对象则为返回的对象 B
- 否:最终 new 出来的对象为最初创建的对象 A
因此当我们执行
let o = new Foo()
实际上执行的是
// 1. 创建一个新的空对象
let A = {};
// 2. 往空对象挂载构造函数 con 的原型对象:A.__proto__ === Con.prototype
Object.setPrototypeOf(A, Con.prototype)
// 3. 执行构造函数: 改变构造函数 this 指向,指向对象A,往A注入属性
let B = Con.apply(A,args)
// 4. 判断构造函数是否返回对象:是则取返回值,否则取最初创建的对象A
const newObj = B instanceof Object ? B : A
手写一个myNew 函数,实现上述操作
const myNew = (Con, ...args) => {
// 1. 创建一个空对象
let A = {};
// 2. 往空对象挂载构造函数 Con 的原型对象: A.__proto__ === Con.prototype
Object.setPrototypeOf(A, Con.prototype);
// 3. 执行构造函数:改变构造函数 this 指向,之啊想对象 A ,往 A 中注入属性
let B = Con.apply(A, args)
// 4. 判断构造函数是否返回对象,是则取返回值,否则取最初创建的对象 A
const newObj = B instanceof Object ? B : A;
return newObj;
}