分享JavaScript 中的几种继承方式
作者:大力yy 发布时间:2024-06-18 03:47:10
前言:
说到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()
,原型包含引用值属性colors
。Son
对象实例自身并没有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 基本思想
在子类构造函数中调用父类构造函数。主要是通过
call
和apply
来实现。
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)}
紧接着上述代码,我们可以看到子类构造函数的原型对象上有name
和colors
属性,但是每个Son
对象实例上都有自身的name
和colors
属性,并不是继承自原型。所以,子类构造函数的原型对象上有name
和colors
属性是多余的。
子类构造函数原型(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)}
来源:https://juejin.cn/post/7093338551835688990


猜你喜欢
- 背景:作为一个python小白,今天从菜鸟教程上看了一些python的教程,看到了python的一些语法,对比起来(有其他语言功底),感觉还
- 一、view实现计数在apiviews.py中加入以下内容from ApiTest.models import ApiTestfrom dj
- map()是一个 Python 内建函数,它允许你不需要使用循环就可以编写简洁的代码。一、Python map() 函数这个map()函数采
- 发送邮件概述:Django中内置了邮件发送功能,发送邮件需要使用SMTP服务,常用的免费服务器有:163、126、QQ注册并登陆163邮箱打
- 1. os.listdir()概述os.listdir() 方法用于返回指定的文件夹包含的文件或文件夹的名字的列表。例如:dir ='
- Python web应用想要发布使用iis发布有两种方式,这篇文章就为大家介绍一下这两种方式的具体实现:1.配置HttpPlatform程序
- 最近合成大西瓜非常火,很多编程爱好者将大西瓜改成了各种版本,非常魔性,哈哈。如果你也想魔改大西瓜,或者想研究一下项目怎么玩的,下面的教程从下
- 前言办公中,偶尔会碰到一种情况,需要提取word文档中的图片,决定写这样一款工具自动提取图片。关于脚本的使用:情景1:如果你拿到的是一个文件
- 一、使用步骤 1.引入库(安装Python环境、PyQt、PyQt-tools)from PyQt5 import QtCore,
- 游戏开始前的注意事项1:游戏《外星人入侵》将包含很多文件,请在你的D盘中新建一个空文件夹,并将其命名为alien_invasion.请务必将
- 1、实现的效果示例代码:df=pd.DataFrame({'A':[1,2],'B':[[1,2],[1,2
- 目录1.celery异步消息队列介绍celery应用举例Celery有以下优点Celery 特性2.工作原理 *****Celery 扮演生
- 本文实例为大家分享了Python实现简单的2048小游戏的具体代码,供大家参考,具体内容如下运行效果:1.项目结构2.代码configs.p
- 函数声明为:func Notify(c chan<- os.Signal, sig ...os.Signal)官方描述:Notify函
- 小主我总结了一下,看官仅供参考。具体运行时间,要看电脑,程序复杂程度,截图大小,原本为四个方法,后面又发现了一种。补上运行熟练度等因素。方法
- 锟拷码和口字码说到乱码问题就不得不提到锟斤拷,这算是非常常见的一种乱码形式,那么它到底是经过何种错误操作产生的呢?下面我们一步步探究。看一个
- 写在前面的话作为有个 Python 菜逼,之前一直用的 Pycharm,但是在主题这一块怎么调整都感觉要么太骚,看起来不舒服,要么就是简直不
- char(n)是定长格式,格式为char(n)的字段固定占用n个字符宽度,如果实际存放的数据长度超过n将被截取多出部分,如果长度小于n就用空
- 很久以前我们在写sql的时候,最怕的一件事情就是sql莫名奇妙的超级慢,慢的是撸一管子回来,那个小球还在一直转。。。这个着急也只有当事人才明
- lambda表达式python中形如:lambda parameters: expression称为lambda表达式,用于创建匿名函数,该