资讯专栏INFORMATION COLUMN

JavaScript · 原型继承

Profeel / 1011人阅读

摘要:这样肯定不行,给添加方法或影响到这种方式有一个缺点,在一个实例时会调用两次构造函数一次是,另一次是,浪费效率,且如果构造函数有副作用,重复调用可能造成不良后果。

写在前面

此文只涉及基于原型的继承,ES6之后基于Class的继承请参考相关文献。

知识储备
构造函数的两种调用方式(结果完全不同)

通过关键字new调用:

function Person(name) {
    this.name = name;
    this.age = 18;
}
var o = new Person("hx");
console.log(o.name, o.age);
// hx 18
console.log(window.name, window.age);
// "" undefined

直接调用:

function Person(name) {
    this.name = name;
    this.age = 18;
}
var o = Person("hx");
console.log(o);
// undefined
console.log(window.name, window.age);
// hx 18

由此可见:

构造函数与普通函数无异,可直接调用,无返回值,this指向Window;

通过new调用的话,返回值为一个对象,且this指向该对象

new到底做了什么?

new关键字会进行如下操作:

创建一个空对象;

链接该对象到另一个对象(即:设置该对象的构造函数);

将第一步创建的空对象作为this的上下文(this指向该空对象);

执行构造函数(为对象添加属性),并返回该对象

function Person(name) {
    this.name = name;
    this.age = 18;
}
var o = new Person("hx");

上述代码对应的四步操作是:

var obj = {};

obj.__proto__ = Person.prototype;

Person.call(obj,"hx");

return obj;

JavaScript实现继承的几种方式

1.原型链继承

function Parent(name) {
    this.name = name;
    this.age = 18;
    this.arr = ["hello","world"]
}
Parent.prototype.sayAge = function() {
    console.log(this.age)
}

function Child(gender) {
    this.gender = gender;
}
Child.prototype = new Parent();

var child1 = new Child("male");
child1.arr.push("js")
console.log(child1.name); // undefined
console.log(child1.age); // 18
console.log(child1.arr); // ["hello","world","js"]
console.log(child1.gender); // male
child1.sayAge(); // 18

var child2 = new Child("female");
console.log(child2.name); // undefined
console.log(child2.age); // 18
console.log(child2.arr); // ["hello","world","js"]
console.log(child2.gender); // female
child2.sayAge(); // 18

优点:

Parent原型对象上的方法可以被Child继承

缺点:

Parent的引用类型属性会被所有Child实例共享,互相干扰

Child无法向Parent传参

2.构造函数继承(经典继承)

function Parent(name) {
    this.name = name;
    this.age = 18;
    this.arr = ["hello","world"];
    this.sayName = function() {
        console.log(this.name)
    }
}
Parent.prototype.sayAge = function() {
    console.log(this.age)
}

function Child(name,gender) {
    Parent.call(this,name); // this由Window指向待创建对象
    this.gender = gender;
}

var child1 = new Child("lala","male");
child1.arr.push("js");
console.log(child1.name); // lala
console.log(child1.age); // 18
console.log(child1.arr); // ["hello","world","js"]
console.log(child1.gender); // male
child1.sayName(); // 18
child1.sayAge(); // Uncaught TypeError: child1.sayAge is not a function

var child2 = new Child("fafa","female");
console.log(child2.name); // fafa
console.log(child2.age); // 18
console.log(child2.arr); // ["hello","world"]
console.log(child2.gender); // female
child2.sayName(); // 18
child2.sayAge(); // Uncaught TypeError: child1.sayAge is not a function

优点:

避免了引用类型属性被所有Child实例共享

Child可以向Parent传参

缺点:

Parent原型对象上的方法无法被Child继承

每次创建Child实例都会创建sayName方法,造成内存资源的浪费

3.组合继承

function Parent(name,age) {
    this.name = name;
    this.age = age;
    this.arr = ["hello","world"]
}
Parent.prototype.sayName = function() {
    console.log(this.name)
}

function Child(name,age,gender) {
    Parent.call(this,name,age);
    this.gender = gender
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constuctor = Child;
Child.prototype.sayAge = function() {
    console.log(this.age)
}

var child1 = new Child("lala",18,"male");
child1.arr.push("js");
child1.name; // "lala"
child1.age; // 18
child1.arr; // ["hello","world","js"]
child1.gender; // "male"
child1.sayName(); // lala
child1.sayAge(); // 18

var child2 = new Child("fafa",28,"female");
child1.name; // "fafa"
child1.age; // 28
child1.arr; // ["hello","world"]
child1.gender; // "female"
child1.sayName(); // fafa
child1.sayAge(); // 28

组合继承是JavaScript继承的最佳实践

属性使用构造函数继承 - 避免了Parent引用属性被多个Child实例影响,同时支持传参

方法使用原型链继承 - 支持Child继承Parent原型对象方法,避免了多实例中方法的重复拷贝

补充 1

对于组合继承代码中的Child.prototype = Object.create(Parent.prototype),还有两种类型的方法:

Child.prototype = Parent.prototype或者Child.prototype = new Parent()

Child.prototype = Parent.prototype:这样肯定不行,给Child.prototype添加方法或影响到Parent;

Child.prototype = new Parent():这种方式有一个缺点,在new一个Child实例时会调用两次Parent构造函数(一次是new Parent(),另一次是Parent.call(this,name)),浪费效率,且如果Parent构造函数有副作用,重复调用可能造成不良后果。

对于第二种情况,除了使用Object.create(Parent.prototype)这种方法外,还可以借助一个桥接函数实现。实际上,不管哪种方法,其实现思路都是调整原型链:

由:
new Child() ----> Child.prototype ----> Object.prototype ----> null

调整为:
new Child() ----> Child.prototype ----> Parent.prototype ----> Object.prototype ----> null

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

function Child(name,age) {
    Parent.call(this,name);
    this.age = age;
}

function F() {
}

F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constuctor = Child;

Child.prototype.sayAge = function() {
    console.log(this.age)
}

可见,通过一个桥接函数F,实现了只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性

// 封装一下上述方法
function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

function prototype(child, parent) {
    var prototype = object(parent.prototype);
    child.prototype = prototype;
    prototype.constructor = child;
}

// 当我们使用的时候:
prototype(Child, Parent);
补充 2

什么是最优的继承方式?

其实不管是改良的组合继承(使用 Object.create 也好,还是使用 Object.setPrototypeOf 也好),还是所谓的寄生组合继承(使用桥接函数F),都不是回答该问题的关键。

最优的继承方式体现的是一种设计理念:
不分静态属性还是动态属性,其维度的划分标准是:是否可共享

对于每个子类都有,但子类实例相互独立的属性(非共享):应该++放到父类的构造方法上++,然后通过子类调用父类构造方法来实现初始化;

对于每个子类都有,且子类实例可以共享的属性(不管是静态属性还是动态属性):应该++放到父类的原型对象上++,通过原型链获得;

对于每个子类独有,且子类实例相互独立的属性(非共享):应该++放到子类的构造方法上++实现;

对于每个子类独有,但子类实例可以共享的属性:应该++放到子类的原型对象上++,通过原型链获得;

从文字上不容易理解,看代码:

function Man(name,age) {
    // 每个子类都有,但相互独立(非共享)
    this.name = name;
    this.age = age;
}

Man.prototype.say = function() {
    // 每个子类都有,且共享的动态属性(共享)
    console.log(`I am ${this.name} and ${this.age} years old.`)
}
// 每个子类都有,且共享的静态属性(共享)
Man.prototype.isMan = true;

function Swimmer(name,age,weight) {
    Man.call(this,name,age);
    // Swimmer子类独有,且各实例独立(非共享)
    this.weight = weight;
}

function BasketBaller(name,age,height) {
    Man.call(this,name,age);
    // BasketBaller子类独有,且各实例独立(非共享)
    this.height = height;
}

// 使用ES6直接设置原型关系的方法来构建原型链
Object.setPrototypeOf(Swimmer.prototype, Man.prototype)
// 等同于 Swimmer.prototype = Object.create(Man.prototype); Swimmer.prototype.constructor = Swimmer;
Object.setPrototypeOf(BasketBaller.prototype, Man.prototype)
// 等同于 BasketBaller.prototype = Object.create(Man.prototype); BasketBaller.prototype.constructor = BasketBaller;

// 继续扩展子类原型对象
Swimmer.prototype.getWeight = function() {
    // Swimmer子类独有,但共享的动态属性(共享)
    console.log(this.weight);
}
// Swimmer子类独有,但共享的静态属性(共享)
Swimmer.prototype.isSwimmer = true;

var swimmer1 = new Swimmer("swimmer1",11,100);
var swimmer2 = new Swimmer("swimmer2",21,200);

swimmer1; // Swimmer {name: "swimmer1", age: 11, weight: 100}
swimmer1.isMan; // ture
swimmer1.say(); // I am swimmer1 and 11 years old.
swimmer1.isSwimmer; // ture
swimmer1.getWeight(); // 100

swimmer2; // Swimmer {name: "swimmer2", age: 21, weight: 200}
swimmer2.isMan; // ture
swimmer2.say(); // I am swimmer2 and 21 years old.
swimmer2.isSwimmer; // ture
swimmer2.getWeight(); // 200

// BasketBaller同理(略)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/106315.html

相关文章

  • 理解JavaScript的核心知识点:原型

    摘要:首先,需要来理清一些基础的计算机编程概念编程哲学与设计模式计算机编程理念源自于对现实抽象的哲学思考,面向对象编程是其一种思维方式,与它并驾齐驱的是另外两种思路过程式和函数式编程。 JavaScript 中的原型机制一直以来都被众多开发者(包括本人)低估甚至忽视了,这是因为绝大多数人没有想要深刻理解这个机制的内涵,以及越来越多的开发者缺乏计算机编程相关的基础知识。对于这样的开发者来说 J...

    iKcamp 评论0 收藏0
  • 白话解释 Javascript 原型继承(prototype inheritance)

    摘要:我们有了构造函数之后,第二步开始使用它构造一个函数。来个例子这种方式很简单也很直接,你在构造函数的原型上定义方法,那么用该构造函数实例化出来的对象都可以通过原型继承链访问到定义在构造函数原型上的方法。 来源: 个人博客 白话解释 Javascript 原型继承(prototype inheritance) 什么是继承? 学过面向对象的同学们是否还记得,老师整天挂在嘴边的面向对象三大特...

    kid143 评论0 收藏0
  • 彻底搞懂JavaScript中的继承

    摘要:这正是我们想要的太棒了毫不意外的,这种继承的方式被称为构造函数继承,在中是一种关键的实现的继承方法,相信你已经很好的掌握了。 你应该知道,JavaScript是一门基于原型链的语言,而我们今天的主题 -- 继承就和原型链这一概念息息相关。甚至可以说,所谓的原型链就是一条继承链。有些困惑了吗?接着看下去吧。 一、构造函数,原型属性与实例对象 要搞清楚如何在JavaScript中实现继承,...

    _ivan 评论0 收藏0
  • [译] 为什么原型继承很重要

    摘要:使用构造函数的原型继承相比使用原型的原型继承更加复杂,我们先看看使用原型的原型继承上面的代码很容易理解。相反的,使用构造函数的原型继承像下面这样当然,构造函数的方式更简单。 五天之前我写了一个关于ES6标准中Class的文章。在里面我介绍了如何用现有的Javascript来模拟类并且介绍了ES6中类的用法,其实它只是一个语法糖。感谢Om Shakar以及Javascript Room中...

    xiao7cn 评论0 收藏0
  • JavaScript继承方式及优缺点

    摘要:继承简介在的中的面向对象编程,继承是给构造函数之间建立关系非常重要的方式,根据原型链的特点,其实继承就是更改原本默认的原型链,形成新的原型链的过程。 showImg(https://segmentfault.com/img/remote/1460000018998684); 阅读原文 前言 JavaScript 原本不是纯粹的 OOP 语言,因为在 ES5 规范中没有类的概念,在 ...

    nanchen2251 评论0 收藏0
  • Javascript 设计模式读书笔记(三)——继承

    摘要:的继承方式属于原型式继承,非常灵活。当使用关键字执行类的构造函数时,系统首先创建一个新对象,这个对象会继承自构造函数的原型对象新对象的原型就是构造函数的属性。也就是说,构造函数用来对生成的新对象进行一些处理,使这个新对象具有某些特定的属性。 继承这个东西在Javascript中尤其复杂,我掌握得也不好,找工作面试的时候在这个问题上栽过跟头。Javascript的继承方式属于原型式继承,...

    cangck_X 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<