分享JavaScript 中的几种继承方式

目录
  • 一、原型链
    • 1.1 原型链的问题
  • 二、盗用构造函数
    • 2.1 基本思想
    • 2.2 可向父类构造函数传参
    • 2.3 盗用构造函数的问题
  • 三、组合继承(伪经典继承)
    • 3.1 基本思想
    • 3.2 组合继承的问题
  • 四、原型式继承
    • 4.1 基本思想
    • 4.2 Object.create()
      • (1)语法
      • (2)示例
      • (3)手动实现
  • 五、寄生式继承
    • 5.1 基本思想
    • 5.2 寄生式继承
  • 六、寄生式组合继承
    • 6.1 基本思想

前言:

说到JavaScript中的继承,与之密切相关的就是原型链了,JavaScript中的继承主要是通过原型链实现的。但是简单的原型链继承方式也存在一定的缺陷,在此借着《JavaScript高级程序设计(第四版)》一书,聊聊JavaScript中的几种继承方式

一、原型链

ECMA-262 把原型链定义为ECMAScript的主要继承方式,其基本思想就是通过原型继承多个引用类型的属性和方法。

在此回顾一下原型、构造函数、实例之间的关系:

每个构造函数都有一个原型对象,原型有一个属性指回构造函数,实例有一个内部指针指向原型。
有关原型和原型链的知识这里先不多说了,这里来谈谈原型链的一些问题。

1.1 原型链的问题

  • 原型链主要问题出现在原型中包含引用值的时候。因为原型上的属性会在所有属性之间共享,对于原型上的引用值,实例继承的是指向该对象的引用,所以在实例中修改该属性时,会影响原型上的属性。
function Father() {
    this.colors = ['red'];
}
function Son() {}
Son.prototype = new Father();
let son1 = new Son();
console.log(son1.colors);  // ['red']
son1.colors.push('green');
console.log(son1.colors);  // ['red', 'green']
console.log(son1.hasOwnProperty('colors'));  // false
let son2 = new Son();
console.log(son2.colors);  // ['red', 'green']
console.log(Son.prototype.colors);  // ['red', 'green']

如上代码,构造函数的原型为new Father(),原型包含引用值属性colorsSon对象实例自身并没有colors属性,而是继承自原型,所以向colors中添加"green"影响到的原型上的colors。这就导致son2访问colors属性时值为['red', 'green']
所以,若原型上属性为引用值时,在实例中对该属性修改时会影响原型属性。

但是需要注意下面这种情况:

function Father() {
    this.colors = ['red'];
}
function Son() {}
Son.prototype = new Father();
let son1 = new Son();
console.log(son1.colors);  // ['red']
son1.colors = [];
console.log(son1.colors);  // []
console.log(son1.hasOwnProperty('colors'));  // true
let son2 = new Son();
console.log(son2.colors);  // ['red']
console.log(Son.prototype.colors);  // ['red']

代码中son1.colors = []并不是修改原型属性colors[],而是在为实例son1添加新的属性colors

  • 原型链的另一个问题是,子类型在实例化时不能给父类型的构造函数传参。即不能在不影响其他对象实例的情况下传递参数给父类的构造函数。

那上面的代码来说就是,在创建Son对象实例的时候,不能指定colors的值。

综上所述:由于引用值和传参问题,原型链一般不会被单独使用。

二、盗用构造函数

为了解决原型包含引用值所导致的问题,出现了一种叫作"盗用构造函数"(constructor stealing)的技术。

2.1 基本思想

在子类构造函数中调用父类构造函数。主要是通过callapply来实现。

function Father() {
    this.colors = ['red'];
}
function Son() {
    // 在此通过call()调用父类构造函数
    Father.call(this);
}
let son1 = new Son();
console.log(son1.colors);  // ['red']
// 说明colors 是实例的自身属性
console.log(son1.hasOwnProperty('colors'));  // true
son1.colors.push('green');
console.log(son1.colors);  // ['red', 'green']
let son2 = new Son();
console.log(son2.colors);  // ['red']

new运算符调用构造函数的过程可知,会将函数中的this指向新创建的实例。所以Father.call(this);相当于实例调用了Father方法,然后添加了自身属性colors。所以后续son1.colors.push('green');并不会影响到其他实例。

2.2 可向父类构造函数传参

盗用构造函数的另外一个优点在于,可以在子类构造函数中向父类构造函数传参。

如下代码:

function Father(name) {
    this.name = name;
}
function Son(name) {
    Father.call(this, name);
}
let son = new Son('dali');
console.log(son);  // Son {name: 'dali'}

2.3 盗用构造函数的问题

盗用构造函数的主要问题如下:

  • 所有方法必须在构造函数中定义,所以方法不能重用。(即:即使功能相同的方法,每个实例上对应的该方法不是同一个函数对象)
function Father() {
    this.foo = function() {}
}
function Son() {
    Father.call(this);
}
let son1 = new Son();
let son2 = new Son();
console.log(son1.foo === son2.foo);  // false
  • 子类不能访问到父类原型上的方法。因为子类仅仅只是调用父类构造函数,并没有设置原型指向父类实例。子类和父类之间并没有建立原型关系。
let son = new Son();
console.log(son instanceof Father)  // false

综上所述:单独使用盗用构造函数也是不可行的。

三、组合继承(伪经典继承)

3.1 基本思想

组合继承综合了原型链和盗用构造函数,使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。

function Father(name) {
    this.name = name;
    this.colors = ['red'];
}
Father.prototype.sayHello = function() {
    console.log('hello');
}
function Son(name) {
    // 继承属性
    Father.call(this, name);
}
// 构建原型链,继承方法
Son.prototype = new Father();
let son1 = new Son('dali');
console.log(son1);  // {name: 'dali', ['red']}
son1.colors.push('green');
console.log(son1);  // {name: 'dali', ['red', 'green']}
let son2 = new Son('haha');
console.log(son2);  // {name: 'haha', ['red']}
// 每个实例都有自身的 colors 属性
console.log(son1.colors === son2.colors)  // false
// 实例间共享sayHello方法
console.log(son1.sayHello === son2.sayHello)  // true

通过调用父类构造函数,每个实例都有“自身”的原型属性(例如:colors),所以通过引用修改对应的对象时,不会影响其他实例,因为每个实例的引用值属性指向的对象不同。此外,通过原型链也实现了所以实例之间共享同一方法。

3.2 组合继承的问题

虽然组合继承弥补了原型链和盗用构造函数的不足,但是组合继承也存在效率问题:

  • 父类的构造函数会被调用两次

    • 一次时在创建子类原型的时候被调用
    • 另一次是实例化子类对象时在子类构造函数中被调用
  • 子类原型上存在不必要的属性
console.log(Son.prototype);  // Father {name: undefined, colors: Array(1)}

紧接着上述代码,我们可以看到子类构造函数的原型对象上有namecolors属性,但是每个Son对象实例上都有自身的namecolors属性,并不是继承自原型。所以,子类构造函数的原型对象上有namecolors属性是多余的。

  • 子类构造函数原型(prototype)上的constructor属性丢失
console.log(Son.prototype.constructor === Son)  //  false

修改构造函数的原型都会出现这种问题。

四、原型式继承

4.1 基本思想

function object(o) {
    function F();
    F.prototype = o;
    return new F();
}

其实就是在创建一个对象时,指定该对象的原型。

4.2 Object.create()

在ECMAScript 5 中增加了Object.create()方法,对原型式继承进行了规范化

Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。

(1)语法

Object.create(proto,[propertiesObject])
  • proto: 新创建对象的原型对象
  • propertiesObject: 可选。需要传入一个对象,该对象的属性类型参照Object.defineProperties()的第二个参数。如果该参数被指定且不为 undefined,该传入对象的自有可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象添加指定的属性值和对应的属性描述符。
  • 返回值:一个新对象,带着指定的原型对象和属性。

(2)示例

o = Object.create(Object.prototype, {
  // foo会成为所创建对象的数据属性
  foo: {
    writable:true,
    configurable:true,
    value: "hello"
  },
  // bar会成为所创建对象的访问器属性
  bar: {
    configurable: false,
    get: function() { return 10 },
    set: function(value) {
      console.log("Setting `o.bar` to", value);
    }
  }
});

(3)手动实现

function objectCreate(proto, propertiesObject=undefined){
    // 构造函数
    function F() {
    }
    // 构造函数原型 prototype 链接到proto对象
    F.prototype = proto;

    // 创建对象
    const obj = new F();
    // 若参数 propertiesObject 被指定且不为 undefined
    if (propertiesObject !== undefined) {
        // 新创建的对象添加指定的属性值和对应的属性描述符。
        Object.defineProperties(obj, propertiesObject);
    }
    return obj;
}

五、寄生式继承

5.1 基本思想

创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

function createAnother(original) {
    // 通过调用函数创建一个新对象
    let clone = Object(original);
    // 以某种方式增强这个对象
    clone.sayHi = function() {
        console.log('hi');
    };

    // 返回增强的对象
    return clone;
}

个人理解:寄生式继承就是通过一个函数,以当前对象为基础,创建一个新的对象,并为新的对象添加新的方法。

let obj = {};
let anotherObj = createAnother(obj);
anotherObj.sayHi();  // hi

5.2 寄生式继承

与盗用构造函数类似,寄生式继承中给对象新增的函数不能被重用。

六、寄生式组合继承

针对第三节中组合继承存在的问题,可以通过寄生式组合继承来解决。

6.1 基本思想

不通过调用父类构造函数给子类原型赋值,而是得到父类原型的一个副本。即使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。

function inheritPrototype(subType, SuperType) {
    // 创建对象
    let prototype = Object(SuperType.prototype);
    // 增强对象(防止修改原型导致constructor丢失)
    prototype.constructor = subType;
    // 赋值对象
    subType.prototype = prototype
}
  • subType:子类构造函数
  • SuperType:父类构造函数

如上代码:

  • 首先创建一个父类原型的副本
  • 在副本上添加constructor属性,防止在修改原型时丢失了constructor属性
  • 最后修改子类构造函数的原型,实现继承
function Father(name) {
    this.name = name;
    this.colors = ['red'];
    console.log('父类构造函数调用了');
}
Father.prototype.sayHello = function() {
    console.log('hello');
}
function Son(name) {
    // 继承属性
    Father.call(this, name);
}
// 寄生式继承原型
inheritPrototype(Son, Father)

// 父类构造函数只在实例化时调用一次
let son = new Son('dali');  // 父类构造函数调用了

// 子类构造函数中不存在不必要的属性
console.log(Son.prototype)  // {sayHello: ƒ, constructor: ƒ}
// 子类构造函数的 constructor 属性未丢失
console.log(Son.prototype.constructor === Son)  // true

如上代码,寄生式组合继承解决了组合继承存在的一些问题。综上,寄生式组合继承可以算是引用类型继承的最佳模式。

但是,关于寄生式组合需要注意的一点是:寄生式继承函数在创建对象副本时,如果使用的是Object()函数,对于Object()函数如果给定值是一个已经存在的对象,则会返回这个已经存在的值(相同地址)。所以函数中prototype.constructor = subType;会修改父类原型上的constructor属性。

console.log(Father.prototype.constructor)  // ƒ Son(name) {// 继承属性 Father.call(this, name);}
console.log(Father.prototype.constructor === Father)  // false

但是,这并不会影响父类对象实例的创建

console.log(new Father('haha'))  // Father {name: 'haha', colors: Array(1)}

到此这篇关于分享JavaScript 中的几种继承方式的文章就介绍到这了,更多相关JS继承方式内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • JS高级ES6的6种继承方式

    目录 1.原型链继承 2.借助构造函数继承 3.组合式继承 4.原型式继承 5.寄生式继承 6.寄生组合式继承 前言: 继承是面向对象中老生常谈的一个内容,在ECMAScript6之前,JavaScript中的继承可谓是非常的繁琐的,有各种各样的继承,本质上所有的继承都是离不开原型链的,ES6新增的extends关键字也是通过原型链实现的继承,但是语法相对来说就简单了很多. 关于原型链的内容,可以参考上篇文章两张图搞懂原型链 本篇文章就来介绍一下在ECMAScript6之前是怎么实现继承的. 1

  • JavaScript中常见的几种继承方式

    目录 原型继承 内存图 分析 盗用构造函数继承 分析 组合继承 原型链继承 寄生式继承 寄生组合式继承 原型继承 function Parent(name) { this.name = name } Parent.prototype.getName = function(){ return 'parentPrototype' + ' ' + this.name; } function Son() { } Son.prototype = new Parent(); console.log(new

  • JavaScript 常见的继承方式汇总

    原型链机制: 在ECMAscript中描述了原型链的概念,并将原型链作为实现继承的主要方法,其基本思想就是利用原型让一个引用类型继承另一个引用类型的属性和方法. 构造函数和原型还有实例之间的关系: 每个构造函数都有一个原型对象(prototype),原型对象都包含一个指向构造函数的指针(constructor),而实例都包含一个指向原型对象的内部指针 ( __propto__ ) .关系图如下图所示: 每一个Function都是Object基类的一个实例,所以每一个Function上都有一个__

  • js中常见的6种继承方式总结

    目录 前言 1.原型继承 2.盗用构造函数 3.组合继承 4.原型式继承 5.寄生式继承 6.寄生式组合继承 总结 前言 js是门灵活的语言,实现一种功能往往有多种做法,ECMAScript没有明确的继承机制,而是通过模仿实现的,根据js语言的本身的特性,js实现继承有以下通用的几种方式 温馨提示:本文中Super函数都是指父类,Sub函数都是代表子类.同时文中会涉及到“构造函数模式”和“工厂模式”,如果不熟悉的小伙伴,可以先看看这篇介绍 js常见的4种创建对象方式. 1.原型继承 实现: fu

  • JS中的六种继承方式以及优缺点总结

    目录 前言 原型链继承 构造函数继承 组合继承(原型链继承和构造函数继承组合) 寄生式继承 组合寄生式继承 extends继承 总结 前言 继承是JS世界中必不可少的一个环节,号称JS的三座大山之一,使用这种方式我们可以更好地复用以前的开发代码,缩短开发的周期.提升开发效率 在ES6之前,JS中的类都是通过构造函数模拟的,并不存在真正意义上的类,虽然ES6的类知识一个语法糖

  • 分享JavaScript 中的几种继承方式

    目录 一.原型链 1.1 原型链的问题 二.盗用构造函数 2.1 基本思想 2.2 可向父类构造函数传参 2.3 盗用构造函数的问题 三.组合继承(伪经典继承) 3.1 基本思想 3.2 组合继承的问题 四.原型式继承 4.1 基本思想 4.2 Object.create() (1)语法 (2)示例 (3)手动实现 五.寄生式继承 5.1 基本思想 5.2 寄生式继承 六.寄生式组合继承 6.1 基本思想 前言: 说到JavaScript中的继承,与之密切相关的就是原型链了,JavaScript

  • Javascript中的几种继承方式对比分析

    开篇 从'严格'意义上说,javascript并不是一门真正的面向对象语言.这种说法原因一般都是觉得javascript作为一门弱类型语言与类似java或c#之类的强型语言的继承方式有很大的区别,因而默认它就是非主流的面向对象方式,甚至竟有很多书将其描述为'非完全面向对象'语言.其实个人觉得,什么方式并不重要,重要的是是否具有面向对象的思想,说javascript不是面向对象语言的,往往都可能没有深入研究过javascript的继承方式,故特撰此文以供交流. 为何需要利用javascript实现

  • 浅谈js中的三种继承方式及其优缺点

    第一种,prototype的方式: //父类 function person(){ this.hair = 'black'; this.eye = 'black'; this.skin = 'yellow'; this.view = function(){ return this.hair + ',' + this.eye + ',' + this.skin; } } //子类 function man(){ this.feature = ['beard','strong']; } man.pr

  • JavaScript中的几种继承方法示例

    1.原型链继承 原理: 子类原型指向父类实例对象实现原型共享,即Son.prototype = new Father(). 这里先简单介绍下原型 js中每个对象都有一个__proto__属性,这个属性指向的就是该对象的原型.js中每个函数都有一个prototype属性,这个属性指向该函数作为构造函数调用时创建的实例的原型.原型对象上有一个constructor属性,指向创建该对象的构造函数,该属性不可枚举. var obj = {}; obj.__proto__ === Object.proto

  • 详解C++ 中的三种继承方式

    public 方式继承 基类成员对派生类的可见性对派生类来说,基类的公有成员和保护成员可见,基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态;基类的私有成员不可见,基类的私有成员仍然是私有的,派生类不可访问基类中的私有成员. 基类成员对派生类对象的可见性对派生类对象来说,基类的公有成员是可见的,其他成员是不可见的. 所以,在公有继承时,派生类的对象可以访问基类中的公有成员,派生类的成员函数可以访问基类中的公有成员和保护成员. 简单来说,派生类能访问基类的public, prote

  • javascript中的3种继承实现方法

    使用Object.create实现类式继承 下面是官网的一个例子 //Shape - superclass function Shape() { this.x = 0; this.y = 0; } Shape.prototype.move = function(x, y) { this.x += x; this.y += y; console.info("Shape moved."); }; // Rectangle - subclass function Rectangle() {

  • Javascript编程中几种继承方式比较分析

    本文实例分析了Javascript编程中几种继承方式比较.分享给大家供大家参考,具体如下: 开篇 从'严格'意义上说,javascript并不是一门真正的面向对象语言.这种说法原因一般都是觉得javascript作为一门弱类型语言与类似java或c#之类的强型语言的继承方式有很大的区别,因而默认它就是非主流的面向对象方式,甚至竟有很多书将其描述为'非完全面向对象'语言.其实个人觉得,什么方式并不重要,重要的是是否具有面向对象的思想,说javascript不是面向对象语言的,往往都可能没有深入研究

  • 浅谈Javascript中的对象和继承

    Javascript是一门函数式编程语言,Javascript当中函数是核心,在Javascript中函数也是对象,函数对象在创建的时候会被添加属性和方法. 在Javascript中函数对象有两种调用方式,一种是new关键字的调用,另一种是没有new关键字的调用,前者会返回一个对象,后者会返回return语句中的内容. function Obj (name) { this.name = name; return name; } 用new关键字来调用如下: var obj = new Obj('张

随机推荐