首頁  >  文章  >  web前端  >  全面分析javascript繼承的原理

全面分析javascript繼承的原理

不言
不言原創
2018-09-03 10:13:391260瀏覽

這篇文章帶給大家的內容是關於全面分析javascript繼承的原理 ,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。

繼承

我們知道JS是OO編程,自然少不了OO編程所擁有的特性,學習完原型之後,我們趁熱打鐵,來聊聊OO編程三大特性之一-繼承。

繼承這個詞應該比較好理解,我們耳熟能詳的,繼承財產,繼承家業等,他們的前提是有個繼承人,然後你是繼承者,這樣才有繼承而言。沒錯,JS中的繼承就如你所理解的一樣,也是成對出現的。
繼承就是將物件的屬性複製一份給需要繼承的物件

OO語言支援兩種繼承方式:介面繼承和實作繼承,其中介面繼承只繼承方法簽章,而實作繼承則繼承實際的方法。由於ECMAScript中的函數沒有簽名,因此無法實現介面繼承,只支援實現繼承,而繼承的主要方式,是透過原型鏈實現的,要理解原型鏈,首先要知道什麼是原型,不懂的小夥伴,可以看這篇javascript原型是什麼? javascript原型的詳細解說

其實繼承說白了就是
①它上面必須有個父級
②且它取得了這個父級的所有實例和方法

這裡普及一個小概念,上文提到的沒有簽名,第一次看這個字眼也不是很懂,搜尋了一下,覺得這個說法還是比較認可的。

沒有簽章
我們知道JS是弱型別語言,它的參數可以由0個或多個值的陣列來表示,我們可以為JS函數命名參數,這個做法只是為了方便,但不是必須,也就是說,我們命不命名參數和傳不傳參數沒有必然聯繫,既可以命名參數,但不傳(此時默認為undefined),也可以不命名參數,但傳參數,這種寫法在JS中是合法的,反之,強型別語言,對這個要求就非常嚴格,定義了幾個參數就一定要傳幾個參數下去。 命名參數這塊必須要求事先建立函數簽名,而將來的呼叫也必須與該簽名一致。 (也就是說定義幾個參數就要傳幾個下去)**,而js沒有這些條框框,解析器不會驗證命名參數,所以說js沒有簽章。

舉例

function JSNoSignature () {  
  console.log("first params" + arguments[0] + "," + "second params" + arguments[1]);
}
JSNoSignature ("hello", "world");

這個例子很明顯了。命名參數為空,但我們依舊可以傳參,呼叫該方法。所謂的參數類型,參數個數,參數位置,出入參數,js統統不關心,它所有的值都被放到arguments中了,需要返回值的話直接return,不用聲明,這就叫做js沒有簽名。

原型鏈

什麼是原型鏈呢?字面上也很好理解,就是將所有的原型串在一起就叫做原型鏈。當然這個解釋只是為了方便理解罷了,原型鍊是作為實現繼承的主要方法,其基本思想是利用原型一個引用類型繼承另一個引用類型的屬性和方法 。我們知道每個建構函式都有一個原型對象,原型對像都包含一個指向建構函式的指針,而實例都包含一個指向原型的內部指針。此時,如果我們讓原型物件等於另外一個類型的實例,那麼此時的原型物件將包含一個指向另一個原型的指針,相應地,另一個原型中也包含著一個指向另一個建構函數的指針,這種周而復始的連結關係,就構成了實例與原型的鏈條,這就是原型鏈。

一句話說白了,就是實例→原型→實例→原型→實例... 連結下去就是原型鏈。

我覺得繼承就是原型鏈的一種表現形式

我們知道了原型鏈後,要知道他如何去使用,ECMA提供一套原型鏈的基本模式基本模式如下

原型鏈的基本模式

// 创建一个父类
function FatherType(){
    this.fatherName = '命名最头痛';
}

FatherType.prototype.getFatherValue = function() {
    return this.fatherName;
}

function ChildType(){
    this.childName = 'George';
}

// 继承了FatherType,即将一个实例赋值给函数原型,我们就说这个原型继承了另一个函数实例
// 将子类的原型指向这个父类的实例
ChildType.prototype = new FatherType();

ChildType.prototype.getChildValue = function() {
    return this.childName;
}

let instance = new ChildType();
console.log(instance.getFatherValue()); // 命名最头痛

呼叫instance.getFatherValue()時會經歷三個搜尋步驟
①搜尋實例
②搜尋ChildType.prototype
③搜尋FatherType.prototype,此時在這步驟找到該方法,在找不到屬性或方法的情況下,搜尋過程總是要一環一環地向前行到原型鏈末端才會停下來。

此時的原型鍊是instance → ChildType.prototype → FatherType.prototype
執行instance.getFatherValue()後,getFatherValue裡面的this是ChildType,此時ChildType會根據原型鏈去找fatherName屬性,最終在FatherType中找到。
此時instance.constructor是指向FatherType的

#

默认的原型

所有的引用类型默认都继承了Object,而这个继承也是通过原型链实现的,因此,所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object。prototype,这也就是所有自定义类型都会继承toString(),valueOf()等默认方法的根本原因。
Array类型也是继承了Object类型的。
因此,我们可以总结一下,在原型链的最顶端就是Object类型,所有的函数默认都继承了Object中的属性。

原型和实例关系的确认

isPrototypeOf方法

javascript原型是什么?javascript原型的详细解说中我们有提到过isPrototypeOf方法可以用于判断这个实例的指针是否指向这个原型,这一章我们学习了原型链,这里做个补充,按照原型链的先后顺序,isPrototypeOf方法可以用于判断这个实例是否属于这个原型的。

依旧用上面那个例子
// 注意,这里用的是原型,Object.prototype,FatherType.prototype,ChildType.prototype
console.log(Object.prototype.isPrototypeOf(instance)); // true
console.log(FatherType.prototype.isPrototypeOf(instance)); // true
console.log(ChildType.prototype.isPrototypeOf(instance)); // true

下面再介绍另一种方法,通过instanceof操作符,也可以确定原型和实例之间的关系

instanceof操作符

instanceof操作符是用来测试原型链中的构造函数是否有这个实例

function FatherType(){
    this.fatherName = '命名最头痛';
}

FatherType.prototype.getFatherValue = function() {
    return this.fatherName;
}

function ChildType(){
    this.childName = 'George';
}

// 继承了FatherType
ChildType.prototype = new FatherType();

// 创建实例
let instance = new ChildType();

// 为ChildType原型上添加新方法,要放在继承FatherType之后,这是因为new FatherType()会将ChildType原型上添加的新方法全部覆盖掉

ChildType.prototype.getChildValue = function() {
    return this.childName;
}

// 此时getFatherValue被重写了
ChildType.prototype.getFatherValue = function() {
    return true
}

console.log(instance.getFatherValue()); // true

②通过原型链实现继承时,不能使用对象字面量创建原型方法,因为这样会重写原型链。这部分的例子和解释在javascript原型是什么?javascript原型的详细解说中已经表述过了。一样的道理,只不过把原型换成了原型链罢了。

原型链的bug

原型链虽然强大,可以用它来实现继承,但是也是存在bug的,它最大的bug来自包含引用类型值的原型。也就是说原型链上面定义的原型属性会被所有的实例共享。
它还有另外一个bug,即在创建子类型的实例时,不能向父类型(超类型)的构造函数中传递参数。或者说没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。
基于以上这两个原因,实践过程中很少会单独使用原型链

借用构造函数

其设计思想就是在子类型构造函数的内部调用父类(超类)构造函数。
由于函数只不过是在特定环境中执行代码的对象,因此通过apply()和call()方法也可以在(将来)新创建的对象上执行构造函数。

function FatherType() {
  this.name = 'George';
} 

function ChildType() {
  //通过call方法改变this的指向,此时FatherType中的this指的是ChildType,相当于在构造函数中定义自己的属性。
  FatherType.call(this);
}

let instance1 = new ChildType(); 
instance1.name = '命名最头痛';
console.log(instance1.name); // '命名最头痛'

let instance2 = new ChildType();
console.log(instance2.name); // George

通过上述方法很好解决了原型属性共享问题,此外,既然是一个函数,它也能传相应的参数,因此也能实现在子类型构造函数中向超类型构造函数传递参数。

function FatherType(name){
  this.name = name
}
function ChildType(){
  FatherType.call(this, "George");
  this.age = 18
}
let instance = new ChildType();
console.log(instance.name);  // George
console.log(instance.age);   // 18

借用构造函数的问题
借用构造函数,方法都在构造函数中定义,那么函数的复用就无从谈起,而且在父类(超类型)的原型定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。

组合继承

组合继承也叫伪经典继承,其设计思想是将原型链和借用构造函数的技术组合到一块,发挥二者之长的一种继承模式,其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承,这样既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

function FatherType(name){
  this.name = name
  this.colors = ['red', 'blue', 'green']
}

FatherType.prototype.sayName = function() {
  console.log(this.name)
}

// 借用构造函数实现对实例的继承
function ChildType(name, age){
  // 使用call方法继承FatherType中的属性
  FatherType.call(this, name);
  this.age = age
}

// 利用原型链实现对原型属性和方法的继承
ChildType.prototype = new FatherType(); //将FatherType的实例赋值给ChildType原型
ChildType.prototype.constructor = ChildType; // 让ChildType的原型指向ChildType函数
ChildType.prototype.sayAge = function(){
  console.log(this.age)
}
let instance1 = new ChildType('命名最头痛', 18);
instance1.colors.push('black');
console.log(instance1.colors);         // 'red, blue, green, black'
instance1.sayName();
instance1.sayAge();

var instance2 = new ChildType('命名最头痛', 18);
console.log(instance2.colors);         // 'red, blue, green'
instance2.sayName();                   // '命名最头痛'
instance2.sayAge();                    // 18

组合继承方式避免了原型链和借用构造函数的缺陷,是JS中常用的继承方式。

原型链继承

原型链继承没有使用严格意义上的构造函数,其思想是基于已有的对象创建新对象

// 此object函数返回一个实例, 实际上object()对传入其中的对象执行了一次浅复制.
function object(o) {
  function F() {}  // 创建一个临时构造函数
  F.prototype = o; // 将传入的对象作为构造函数的原型
  return new F();  // 返回这个临时构造函数的新实例
}

let demo = {
  name: 'George',
  like: ['apple', 'dog']
}

let demo1 = object(demo);
demo1.name = '命名';     // 基本类型
demo1.like.push('cat'); // 引用类型共用一个内存地址

let demo2 = object(demo);
demo2.name = '头痛';    // 基本类型
demo2.like.push('chicken') // 引用类型共用一个内存地址
console.log(demo.name) // George
console.log(demo.like) // ["apple", "dog", "cat", "chicken"]

原型链继承的前提是必须要有一个对象可以作为另一个对象的基础。通过object()函数生成新对象后,再根据需求对新对象进行修改即可。 由于新对象(demo1, demo2)是将传入对象(demo)作为原型的,因此当涉及到引用类型时,他们会共用一个内存地址,引用类型会被所有实例所共享,实际上相当于创建了demo对象的两个副本。

Object.create()方法

ECMA5中新增Object.create()方法规范化了原型式继承。该方法接收两个参数
①基础对象,这个参数的实际作用是定义了模板对象中有的属性,就像上面例子中的demo,只有一个参数情况下,Object.create()与上例子中的object相同
②这个是可选参数,一个为基础对象定义额外属性的对象, 该对象的书写格式与Object.defineProperties()方法的第二个参数格式相同,每个属性都是通过自己的描述符定义的,以这种方式指定的任何属性都会覆盖原型对象上的同名属性。

// 只有一个参数
var demoObj = {
  name: 'George',
  like: ['apple', 'dog', 'cat']
}
let demo1Obj = Object.create(demoObj);
demo1Obj.name = '命名';
demo1Obj.like.push('banana');

let demo2Obj = Object.create(demoObj);
demo2Obj.name = '头痛';
demo2Obj.like.push('walk');

console.log(demoObj.like) //["apple", "dog", "cat", "banana", "walk"]

// 两个参数
var demoObj = {
  name: 'George',
  like: ['apple', 'dog', 'cat']
}

let demo1Obj = Object.create(demoObj, {
  name: {
    value:'命名'
  },
  like:{
    value: ['monkey']
  },
  new_val: {
    value: 'new_val'
  }
});
console.log(demoObj.name) // George
console.log(demo1Obj.name) // 命名
console.log(demo1Obj.like) // ["monkey"]
console.log(demo1Obj.new_val) // new_val
console.log(Object.getOwnPropertyDescriptor(demo1Obj,'new_val')) // {value: "new_val", writable: false, enumerable: false, configurable: false}

如果只想让一个对象与另一个对象保持类型的情况下,原型式继承是完全可以胜任的,不过要注意的是,引用类型值的属性始终都会共享相应的值。

寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路,其设计思想与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数内部以某种方式来增强对象,最后返回一个对象。

// 这个函数所返回的对象,既有original的所有属性和方法,也有自己的sayHello方法
function createAnother(original) {
  let clone = Object.create(original);
  clone.sayHello = function(){            
    console.log('HELLO WORLD')
  }
  return clone;
}

let person = {
  name: 'George',
  foods: ['apple', 'banana']
}

let anotherPerson = createAnother(person);
anotherPerson.sayHello();  // HELLO WORLD

使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,这一点与构造函数模式类似。

寄生组合式继承

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混合形式来继承方法。其背后思想:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。说白了就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

function inheritPrototype(childType, fatherType){
  let fatherObj = Object.create(fatherType.prototype);  // 创建对象
  fatherObj.constructor = childType;   // 弥补重写原型而失去的默认constructor属性
  childType.prototype = fatherObj;     // 指定对象
}

上例是寄生组合式继承最简单的形式,这个函数接受两个参数:子类型构造函数和超类型构造函数,在函数内部,①创建了父类型原型的一个副本,②为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性。③将新创建的对象(即副本)赋值给子类型的原型。

function FatherType(name){
  this.name = name;
  this.foods = ['apple', 'banana'];
}
FatherType.prototype.sayName = function(){
  console.log(this.name)
}
function ChildType(name, age){
  FatherType.call(this, name);
  this.age = age;
}
inheritPrototype(ChildType, FatherType);
ChildType.prototype.sayAge = function(){
  console.log(this.age)
}

总结

JS继承的主要方式是通过原型链实现的

实例-原型-实例-原型...无限链接下去就是原型链

所有引用类型的默认原型都是Object

instanceof操作符和isPrototypeOf方法都可以用于判断实例与原型的关系,其区别是,前者用的是原型,后者用的是构造函数

给原型添加方法的代码一定要放在继承之后,这是因为,在继承的时候被继承者会覆盖掉继承者原型上的所有方法

Object.create()方法用于创建一个新对象,其属性会放置在该对象的原型上

继承有6种方式,分别是原型链,借用构造函数,组合继承,原型式继承,寄生式继承和寄生组合式继承

相关推荐:

JavaScript中的继承之类继承_javascript技巧

JavaScript原型和继承

以上是全面分析javascript繼承的原理的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn