前言
对于原型、原型链、原型继承,是每个前端人员必过的一项基础。几乎在面试的时候都会被问到原型、原型链的这些问题,索性写一下文章,把这些问题一次性理解清楚(看多遍别人的文章,不如实际来写写,看时懂,可过两天就会忘记了)。
原型的理解
我们创建的
每个函数都有一个prototype(原型)属性
,这个属性是一个指针,指向一个对象
,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法
。prototype(原型)就是通过调用构造函数而创建的那个对象实例的原型对象
。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。
function Animal(){}
Animal.prototype.name = 'animal';
Animal.prototype.age = '10';
Animal.prototype.sayName = function(){
console.log(this.name);
}
let dog = new Animal();
dog.sayName(); // 输出 animal
let cat = new Animal();
cat.sayName(); // 输出 animal
复制代码
上面使用一段代码来解释原型共享属性和方法。将属性和方法都添加到Animal的prototype中,构造函数变成了空函数。无论是dog实例Animal(),还是cat实例Animal(),都访问的是同一组属性和同一个sayName()函数。当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性。
function Animal(){}
// 常用原型写法
Animal.prototype = {
name: 'animal',
age: '10',
sayName: function(){
console.log(this.name);
}
};
let dog = new Animal();
dog.sayName(); // 输出 animal
let cat = new Animal();
cat.name = 'Tom';
cat.sayName(); // 输出 Tom
复制代码
前面提到:原型(prototype)是通过调用构造函数而创建的那个对象实例的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。如下图所示:
Animal 的每个实例dog和cat都包含一个内部属性,该属性
仅仅指向了Animal.prototype
;换句话说,它们与构造函数没有直接的关系。
原型链的理解
原型链基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
function Animal(){}
// 常用原型写法
Animal.prototype = {
name: 'animal',
age: '10',
sayName: function(){
console.log(this.name);
}
};
// 继承Animal,实际上就是重写原型
function runAnimal(){}
runAnimal.prototype = new Animal();
runAnimal.prototype.run = function(){
console.log("我会跑!!!");
}
let cat = new runAnimal();
console.log(cat.name); // 输出 animal
console.log(cat.run()); // 输出 我会跑!!!
复制代码
我们知道,所有引用类型默认都继承了 Object,而这个继承也是通过原型链实现的。 所以上面的继承可以转化成成下图:
原型继承
子类的原型对象实例父类
基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法。通过子类的原型prototype对父类实例化。
function Animal(){
this.behavior = ['吃饭', '睡觉'];
}
// 常用原型写法
Animal.prototype = {
name: 'animal',
age: '10',
sayName: function(){
console.log(this.name);
}
};
// 继承Animal,实际上就是重写原型、原型实例化父类
function runAnimal(){}
runAnimal.prototype = new Animal();
runAnimal.prototype.run = function(){
console.log("我会跑!!!");
}
let cat = new runAnimal();
console.log(cat.name); // 输出 animal
console.log(cat.run()); // 输出 我会跑!!!
console.log(cat.behavior); // ["吃饭", "睡觉"]
let dog = new runAnimal();
dog.behavior.push('咆哮');
console.log(dog.behavior); // ["吃饭", "睡觉", "咆哮"]
console.log(cat.behavior); // ["吃饭", "睡觉", "咆哮"] =>关注点
复制代码
缺点:
- 一个子类的实例更改子类原型从父类构造函数中继承来的共有属性就会直接影响到其他子类。
- 在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。
构造函数继承
基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法也可以在(将来)新创建的对象上执行构造函数。
// 父类
function Animal(id){
this.behavior = ['吃饭', '睡觉'];
this.id = id;
}
Animal.prototype = {
name: 'animal',
age: '10',
sayName: function(){
console.log('我的编号是:'+this.id);
}
};
// 声明子类
function childAnimal(id){
// 继承父类
Animal.call(this, id);
}
let cat = new childAnimal(100);
console.log(cat.id); // 输出 100
console.log(cat.behavior); // ["吃饭", "睡觉"]
console.log(cat.name); // undifined =>关注点
console.log(cat.sayName()); // error!!! =>关注点
let dog = new childAnimal(101);
dog.behavior.push('咆哮');
console.log(dog.id); // 输出 101
console.log(dog.behavior); // ["吃饭", "睡觉", "咆哮"]
console.log(cat.behavior); // ["吃饭", "睡觉"] =>关注点
复制代码
优缺点
- 借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。
- 在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。
组合继承
指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
// 父类
function Animal(id){
this.behavior = ['吃饭', '睡觉'];
this.id = id;
}
Animal.prototype = {
name: 'animal',
age: '10',
sayName: function(){
console.log('我的编号是:'+this.id);
}
};
// 声明子类
function childAnimal(id){
// 构造函数继承父类
Animal.call(this, id);
}
// 子类的原型对象实例父类
childAnimal.prototype = new Animal();
let cat = new childAnimal(100);
console.log(cat.id); // 输出 100
console.log(cat.behavior); // ["吃饭", "睡觉"]
console.log(cat.name); // animal =>关注点,区别之处
console.log(cat.sayName()); // 我的编号是: 100 =>关注点,区别之处
let dog = new childAnimal(101);
dog.behavior.push('咆哮');
console.log(dog.id); // 输出 101
console.log(dog.behavior); // ["吃饭", "睡觉", "咆哮"]
console.log(cat.behavior); // ["吃饭", "睡觉"] =>关注点
复制代码
优缺点
- 组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继 承模式。而且,instanceof 和 isPrototypeOf()也能够用于识别基于组合继承创建的对象。
- 组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是 在子类型构造函数内部。
原型式继承
原型式继承想法是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
// 道格拉斯·克罗克福德给出的函数
function object(o){
function F(){}
F.prototype = o;
return new F();
}
复制代码
从本质上讲,object()对传入其中的对象执行了一次浅复制。
function book(obj) {
function F(){};
F.prototype = obj;
return new F();
}
let HTML5 = {
name: 'HTML5 高级程序设计',
author: ['Peter Lubbers', 'Ric Smith', 'Frank Salim']
};
let myNewBook = new book(HTML5);
console.log(myNewBook.name); // HTML5 高级程序设计
myNewBook.author.push('Brian Albers');
console.log(myNewBook.author); // ["Peter Lubbers", "Ric Smith", "Frank Salim", "Brian Albers"]
let otherBook = new book(HTML5);
otherBook.name = "VUE";
otherBook.author.push('尤');
console.log(otherBook.name); // VUE
console.log(otherBook.author); // ["Peter Lubbers", "Ric Smith", "Frank Salim", "Brian Albers", "尤"]
console.log(myNewBook.author); // ["Peter Lubbers", "Ric Smith", "Frank Salim", "Brian Albers", "尤"]
复制代码
优缺点
- 父类对象book中的值类型的属性被赋值,引用类型的属性被共同用。引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。
寄生式继承
寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
function createAnother(original){
var clone = object(original); //通过调用函数创建一个新对象
clone.sayHi = function(){ //以某种方式来增强这个对象
console.log("hi");
};
return clone; //返回这个对象
}
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"
复制代码
寄生组合式继承
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
// 定义父类
function SuperClass (name){
this.name = name;
this.colors = ["red","blue","green"];
}
// 定义父类原型方法
SubClass.prototype.getName = function () {
console.log(this.name);
}
// 定义子类
function SubClass (name, time){
SuperClass.call (this, name); // 构造函数式继承
this.time = time; // 子类新增属性
}
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 指定对象
}
// 寄生式继承父类原型
inheriPrototype(SubClass, SuperClass);
// 子类新增原型方法
SubClass.prototype.getTime = function (){
console.log(this.time);
};
// 创建两个测试方法
var instance1 = new SubClass("js book", 2018);
var instance2 = new SubClass("css book", 2019);
instance1.colors.push("black");
console.log(instance1.colors); // ["red","blue","green","black"]
console.log(instance2.colors); // ["red","blue","green"]
instance2.getName (); // css book
instance2.getTime (); // 2019
复制代码
总结
自己慢慢看了一遍小红书,自己手动敲了一下代码,基本上能够理解原型,写的不好,欢迎指正。原型、原型链、原型继承个人感觉就是混合在一起的,提原型,就会提原型链、原型继承。看10遍不如自己动手敲一敲代码,记忆会更深刻,中间出现的问题也会使你眼前一亮(比如你经常使用的this指向问题)。
参考资料
- JavaScript高级程序设计(第3版)
转载于:https://juejin.im/post/5cef3b3b518825526b294b6b