细说 js 的7种继承方式
在这之前,先搞清楚下面这个问题:
function Father(){}
Father.prototype.name = 'father';
Father.prototype.children = []; const child1 = new Father(); console.log('get1 ==',child1); // Father {}
console.log('get ==',child1.name); // father
console.log('get ==',child1.children); // [] child1.name = 'child1';
console.log('set ==',child1.name); // child1 child1.children.push('child2');
// child1.children = ['123'];
console.log('set ==',child1.children); // ["child2"] console.log('get2 ==',child1) // Father {name: "child1"}
疑问:
(1)为什么访问 child1.name 的时候,值是原型上的 name 的值,而设置值之后,实例的原型上的name属性未被修改,反而自己生成了一个name属性?
(2) child1.children.push('child2') 与 child1.children = ['123']; 最终的结果为什么会不同?为什么 push 方法会导致原型上的 children 属性也会改变?
参考:
简单来说就是:
(1)查询对象属性的时候,会从本体对象开始查找,如果有就返回本体上的属性,因为原型链上的被遮蔽了。如果没有就查原型链,直到原型链最高层null,找不到就返回 undefined。
(2)设置值的时候,如果该属性没有通过 Object.defineproperty 设置 setter 或者 writable 为 true,并且本体对象中没有该属性,并且是 ‘=’ 号赋值,那么会直接在本体对象中添加该属性。
(3)所以上面的 name 值,查询的时候是 原型上的 name值。而设置的时候,符合 (2)的条件,所以直接在 child1 中添加 name属性。如果 改成 child1.name++ ,结果也是本体对象中添加新属性。因为 这句代码等价于 child1.name = child1.name + 1; 。是隐形的等号赋值哦。
(4) child1.children.push('child2') 由于不是等号赋值,那么在 执行 child1.children 的时候,查询到 children 之后,没有 “=” 号赋值,而是 push ,所以操作的是 原型对象中的 children 属性(引用属性)。 而 child1.children = ['123'] 也符合(2)的条件。所以是本体对象新增该属性。
(5)可以简单的理解为如果本体对象上没有该属性, ‘=’ 号赋值之后,分配了新的内存地址,因此只有在本体上新增属性,才能保存赋的值。如果有该属性,就是简单的值替换。
上面的问题明白了之后,再来了解一下 js 的几种继承方式。文字有点多,一定要耐心看。几种继承方式是有关系的
1. 原型链继承
function Father(){
this.name = 'father';
this.children = [];
this.age = 30;
}
Father.prototype.say = function(){
if(this.children.length){
console.log('我的孩子:'+this.children.join());
}else{
console.log('我是单身狗')
}
}
function CreateChild(name){
this.name = name || '未出生';
this.age = 0;
// this.children = [];
} CreateChild.prototype = new Father(); // 这句很关键,让子类和父类链接起来,子类继承了父类所有的属性和方法,包括父类原型上的 let child1 = new CreateChild('张三');
let child2 = new CreateChild('张四'); child1.children.push('张小一') console.log(child1)
console.log(child2.children)
结果:
特性:
(1)是通过覆盖构造函数的 原型 prototype 来实现的。
(2)不能给父类传参。
(3)子类继承父类所有的属性和方法,包括 原型 prototype 上的。
(4)这里的继承属性和方法指的是:构造的子实例,本身上是没有属性的(除非自己初始有),只有原型上继承的父类的属性和方法,调用属性或方法是根据原型链查找的。
(5)如果父类有引用类型,子类没有,那么其中一个子类继承来的引用类型修改后会影响所有的子类
说明:
(1)为什么访问 child2.children 的时候,也会出现 child1 的的 children 内容? 因为 访问的时候,child2 本身是没有children 这个属性的,只有在原型上去找,刚好找到父级存在这个属性,而这个属性又是引用属性,一处修改,所有引用的地方都更改了。所以最后结果也是 [‘张小一’];
(2)如果子类里面,自己定义有 children 属性。那么相当于 生成的实例 child1,child2 有些属性就是自己的构造函数上的,并不是继承来的,所以,如果放开 this.children = [] 的注释。你会看到 生成的实例中,父级的 children 属性未变化。
(3)如果在 CreateChild.prototype = new Father() 之后再给 CreateChild.prototype = xxx 赋值的话,结果又会不一样,原型直接被覆盖了。
(4)这里没必要去修复 CreateChild 的 constructor 的指向。因为在 CreateChild.prototype = new Father() 之后,原始 CreateChild 上的原型属性全都被覆盖了,去修复也没什么作用。
(5)那如果我又 想给父类传参 怎么办?借用构造函数继承 就能实现这个需求
2. 借用构造函数继承。
function Father(name){
this.name = name;
this.children = ['张大大'];
this.age = 30;
}
Father.prototype.say = function(){
if(this.children.length){
console.log('我的孩子:'+this.children.join());
}else{
console.log('我是单身狗')
}
}
function CreateChild(name){
// this.children = ['张老大']
Father.call(this,name);
// this.children = ['张老大']
} let child1 = new CreateChild('张三');
let child2 = new CreateChild('张四'); child1.children.push('张大一')
child2.children.push('张小二') console.log(child1)
console.log(child2)
结果:
特性:
(1)继承父类的原始属性和方法,不包括原型上的属性和方法。这里继承的属性和方法指的是:构造的实例,本身继承的是父类的属性和方法。原型上无任何变化。
(2)可以给父类传参,但是不能用于实例化(new)父类的时候传参。
(3)父类的引用属性是独立的, Father.call(this,name) 这段代码,相当于给 Father 方法的 this 绑定 为 CreateChild 函数中 this 的指向。然后给这个指向绑定属性和方法,有点 new 的味道。因为 new 的作用是:内部新增一个对象,让构造函数内部的 this 指向这个新对象,然后执行语句,为这个对象绑定属性和方法,最后返回这个对象。
(4)每次初始化子类都会执行一次 Father 父类,不能复用(一次执行,多次使用)。且子类未用到原型
说明:
(1)最重要,最核心的就是 Father.call(this,name) 这段代码,给子类绑定了属性和方法。
(2)如果放开 CreateChild 里面的第一个或者第二个赋值注释,都会因为代码执行的先后顺序 ,原始的数据会被覆盖。
(3)并不会继承父类原型上的属性和方法。因为 此时的 Father 只是当作普通函数执行,所以 prototype 原型上的属性和方法访问不了,因为 Father 并未使用构造函数的方式执行。
(4)如果我 既想继承父类原型上的属性和方法,又想给父类传参 怎么办?那么 组合继承 就能满足这个需求
3. 组合继承
function Father(name){
this.name = name;
this.children = [];
this.age = 30;
}
Father.prototype.say = function(){
if(this.children.length){
console.log('我的孩子:'+this.children.join());
}else{
console.log('我是单身狗')
}
}
Father.prototype.hobbies = ['woman']; function CreateChild(name){
Father.call(this,name)
} CreateChild.prototype = new Father(); let child1 = new CreateChild('张三');
let child2 = new CreateChild('张四'); child1.children.push('张大一');
child1.hobbies.push('meet'); child2.children.push('张小二');
child2.hobbies.push('fruit') console.log(child1)
console.log(child2)
console.log(child2.hobbies)
结果:
特性:
(1)能继承父类的所有属性和方法,因为 Father.call(this,name) 这句代码。因此,构造实例本身就含有父类的属性和方法。
(2)能继承父类 prototype 原型上的属性和方法,因为 CreateChild.prototype = new Father(); 这句代码 。因此,构造实例的原型上含有父类原型的属性和方法
(3)能给父类传参,但是不能用于实例化(new)父类的时候传参。
(4)父类原型上如果有引用属性,某一实例修改后,其它的实例也会受到影响。
(5)每创建一个实例,Father 函数会被执行一次。
说明:
(1)这种继承方式,是第一,二中方式的 组合,所以叫组合继承。囊括了这两种方式的优缺点。
4. 原型式继承
function extendChild(target){
function Fn(){};
Fn.prototype = target;
return new Fn();
} function Father(){
this.name = 'father';
this.children = [];
this.age = 30;
} const FatherInstance = new Father(); const child1 = extendChild(FatherInstance);
const child2 = extendChild(FatherInstance); child1.children.push('张大一');
child1.name = 'child1'; child2.children = ['张小二'];
child2.name = 'child2'; console.log(child1)
console.log(child2)
结果:
特性:
(1)通过覆盖一个函数的原型,实现构造的实例的原型上继承传入的对象。构造的实例本身是没有属性和方法的。
(2)如果父类有引用属性,那么一个构造实例改变后,其它的实例也会改变。
(3)每次新增实例,都需要执行一次 new Fn()。
(4)主要的功能就是:基于已有的对象,去创建新对象,继承已有对象的属性和方法。
说明:
(1)如果看了文章最初的第一个问题,就会明白child1和child2的name,还有child2的children 属性为什么会添加到本体属性上。
(2)细心的会发现,这种继承方式,和 Object.create 的 polify 一样一样的,是同样的原理,看mdn。
(3)我如果 想给所有实例添加 共同初始的 方法或者属性,而又不影响父类 怎么办? 寄生式继承 就能解决这个问题
5. 寄生式继承
function extendChild(target){
function Fn(){};
Fn.prototype = target;
return new Fn();
} function Father(){
this.name = 'father';
this.children = [];
this.age = 30;
}
const FatherInstance = new Father(); function createChild(target){
var target = extendChild(target)
target.name = 'target';
return target;
}
const child1 = createChild(FatherInstance);
const child2 = createChild(FatherInstance); child1.children.push('张大一');
child1.name = 'child1'; child2.children = ['张小二'];
child2.name = 'child2'; console.log(child1)
console.log(child2)
结果:
特性:
(1)在原型式继承上,多加了一个函数。
(2)可以实例化前,给所有实例添加公用的方法或属性。不会影响父级
说明:
(1)和原型式继承差不多,其它的没看出来有什么优缺点
(2)组合继承挺好的,就是父类多调用了,而寄生式继承 只调用了一次,能不能把寄生式继承的优点和组合继承结合起来?所以 寄生组合式 就这么来了
6. 寄生组合式继承,这种继承方式是最优的继承方式
function Father(name){
this.name = name || 'father';
this.children = [];
this.age = 30;
}
Father.prototype.say = function(){
console.log(this.name);
}
Father.prototype.hobbies = ['fruit'] function Child(name){
Father.call(this,name)
// this.name = name || '未出生';
// this.age = 0;
} function createChild(target){
var Fn = function(){};
Fn.prototype = target;
return new Fn();
} function extendFn(Child,Father){
var instance = createChild(Father.prototype);
Child.prototype = instance;
} extendFn(Child,Father) let child1 = new Child('张三');
let child2 = new Child('张四'); child1.children.push('张大一')
child1.hobbies.push('123')
child1.name = 'child1' child2.children.push('张小二')
child2.name = 'child2' console.log(child1)
console.log(child2)
结果:
特性:
(1)和组合继承的特性一样。
(2)解决了 多次调用 父类的问题。
说明:
(1)createChild 方法 可以换成 Object.create。还可以省点代码,功能是一样的。
7. es6 class类的继承 extends
class Father{
constructor(name){
this.name = name || 'father';
this.children = [];
}
hobbies = ['fruit'];
say(){
console.log(this.name)
}
}
class Child extends Father{
constructor(name) {
super(name);
}
}
let child1 = new Child('child1');
let child2 = new Child('child2'); child1.children.push('child1');
child1.hobbies.push('apple') child2.hobbies = ['apple2'] console.log(child1)
console.log(child2)
console.log(child2.say())
结果:
特性:
(1)很方便用
(2)子类会从父类继承所有的属性和方法,父类的引用属性不共享。
总结:
(1)原型链继承 —(优化)— > 借用构造函数继承 —(优化)—> 组合继承
(2)原型式继承 —(优化)— > 寄生式继承 —(优化)— > 寄生组合式继承
细说 js 的7种继承方式的更多相关文章
- js的6种继承方式
重新理解js的6种继承方式 注:本文引用于http://www.cnblogs.com/ayqy/p/4471638.html 重点看第三点 组合继承(最常用) 写在前面 一直不喜欢JS的OOP,在学 ...
- js的三种继承方式及其优缺点
[转] 第一种,prototype的方式: //父类 function person(){ this.hair = 'black'; this.eye = 'black'; this.skin = ' ...
- 重新理解JS的6种继承方式
写在前面 一直不喜欢JS的OOP,在学习阶段好像也用不到,总觉得JS的OOP不伦不类的,可能是因为先接触了Java,所以对JS的OO部分有些抵触. 偏见归偏见,既然面试官问到了JS的OOP,那么说明这 ...
- js的5种继承方式——前端面试
js主要有以下几种继承方式:对象冒充,call()方法,apply()方法,原型链继承以及混合方式.下面就每种方法就代码讲解具体的继承是怎么实现的. 1.继承第一种方式:对象冒充 function P ...
- js的几种继承方式
1.原型链方式 function Super(){ this.val = 1; this.arr = [1]; } function Sub(){ // ... } Sub.prototype = n ...
- js的2种继承方式详解
js中继承可以分为两种:对象冒充和原型链方式 一.对象冒充包括三种:临时属性方式.call()及apply()方式1.临时属性方式 复制代码代码如下: function Person(name){ ...
- JavaScript_几种继承方式(2017-07-04)
原型链继承 核心: 将父类的实例作为子类的原型 //父类 function SuperType() { this.property = true; } SuperType.prototype.ge ...
- js实现的几种继承方式
他山之石,可以攻玉,本人一直以谦虚的态度学他人之所长,补自己之所短,望各位老师指正! 拜谢 js几种继承方式,学习中的总结: 所谓的继承是为了继承共有的属性,减少不必要代码的书写 第一种:借用构造函数 ...
- 都0202年了,你还不知道javascript有几种继承方式?
前言 当面试官问你:你了解js哪些继承方式?es6的class继承是如何实现的?你心中有很清晰的答案吗?如果没有的话,可以通过阅读本文,帮助你更深刻地理解js的所有继承方式. js ...
随机推荐
- vue通过事件向父级组件发送消息(官网点击放大例子)
注意:Vue.component一定要写在new Vue之前 在页面中使用组件 整体代码示例
- CentOS配置Nginx官方的Yum源
由于yum源中没有我们想要的nginx,那么我们就需要创建一个"/etc/yum.repos.d/nginx.repo"的文件,其实就是新增一个yum源. [root@niaoyu ...
- rocketMq指定broker ip地址,适合解决云主机部署问题
在工作中遇到了一个这个问题,就是我们rocketmq是部署在云主机上的 但是我们的开发同事在自己的电脑连接rocketmq链接不上 报错显示Caused by: org.apache.rocket ...
- day108:MoFang:首页检测用户是否登录&在项目中使用MongoDB&用户页面更新用户信息&交易密码界面实现
目录 1.首页页面也要检测用户是否登录 2.在flask中使用MongoDB 3.用户页面更新用户信息 4.交易密码界面/密码修改界面/昵称修改界面初始化 5.交易密码实现 1.首页页面也要检测用户是 ...
- C# 高性能对象映射
1.之前在使用AutoMapper 框架感觉用着比较不够灵活,而且主要通过表达式树Api 实现对象映射 ,写着比较讨厌,当出现复杂类型和嵌套类型时性能直线下降,甚至不如序列化快. 2.针对AutoMa ...
- unity入门—资源导入与场景创建
前言: 从这一篇章开始,我将会通过游戏实例来讲解如何使用unity制作一个标准的游戏,介绍的内容较多,需要整理的东西也多可能中途会有一两天的咕咕咕,预计想要完成两个游戏,一个射击类一个塔防类,从射击类 ...
- GaussDB(DWS)磁盘维护:vacuum full执行慢怎么办?
摘要:在数据库中用于维护数据库磁盘空间的工具是VACUUM,其重要的作用是删除那些已经标示为删除的数据并释放空间. vacuum的功能 回收空间 数据库总是不断地在执行删除,更新等操作.良好的空间管理 ...
- Java中中文排序器
在Java中使用Collator类按照汉字拼音排序字符串 public static void main(String[] args) throws Exception{ String[] strs ...
- js上 七、表达式
(1).什么是表达式 任何有值的内容都是表达式 一个表达式会产生一个值,它可以放在任何需要一个值的地方,比如a=3中的3就是一个表达式,a=3整体也可以作为一个表达式. 常见表达式有如下几种: ü 原 ...
- Jetty web server 远程共享缓冲区泄漏漏洞学习
https://www.secpulse.com/archives/4911.html https://www.tiejiang.org/11628.html http://blog.gdssecur ...