JS 原型、原型继承、原型链的理解

视野图

前言

对于原型、原型链、原型继承,是每一个前端人员必过的一项基础。几乎在面试的时候都会被问到原型、原型链的这些问题,索性写一下文章,把这些问题一次性理解清楚(看多遍别人的文章,不如实际来写写,看时懂,可过两天就会忘记了)。javascript

原型的理解

咱们建立的每一个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含能够由特定类型的全部实例共享的属性和方法。prototype(原型)就是经过调用构造函数而建立的那个对象实例的原型对象。使用原型对象的好处是可让全部对象实例共享它所包含的属性和方法。css

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属性所在函数的指针。以下图所示: java

函数原型

Animal 的每一个实例dog和cat都包含一个内部属性,该属性仅仅指向了Animal.prototype;换句话说,它们与构造函数没有直接的关系。面试

实例化后的原型

原型链的理解

原型链基本思想是利用原型让一个引用类型继承另外一个引用类型的属性和方法。简单回顾一下构造函数、原型和实例的关系:每一个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。app

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); // ["吃饭", "睡觉", "咆哮"] =>关注点
复制代码

缺点:ui

  • 一个子类的实例更改子类原型从父类构造函数中继承来的共有属性就会直接影响到其余子类。
  • 在建立子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响全部对象实例的状况下,给超类型的构造函数传递参数。

构造函数继承

基本思想至关简单,即在子类型构造函数的内部调用超类型构造函数。别忘了,函数只不过是在特定环境中执行代码的对象,所以经过使用apply()和call()方法也能够在(未来)新建立的对象上执行构造函数。this

// 父类
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版)
相关文章
相关标签/搜索