日志

细说 js 的7种继承方式

 来源    2021-01-14    1  

在这之前,先搞清楚下面这个问题:

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)《你不知道的js》第五章-原型——设置与遮蔽属性 

(2)“=”操作符为对象添加属性

简单来说就是:

(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)和原型式继承差不多,其它的没看出来有什么优缺点

相关文章
重新理解JS的6种继承方式
日志写在前面 一直不喜欢JS的OOP,在学习阶段好像也用不到,总觉得JS的OOP不伦不类的,可能是因为先接触了Java,所以对JS的OO部分有些抵触. 偏见归偏见,既然面试官问到了JS的OOP,那么说明这 ...
2
js的5种继承方式——前端面试
日志js主要有以下几种继承方式:对象冒充,call()方法,apply()方法,原型链继承以及混合方式.下面就每种方法就代码讲解具体的继承是怎么实现的. 1.继承第一种方式:对象冒充 function P ...
1
理解JS的6种继承方式
日志[转]重新理解JS的6种继承方式 写在前面 一直不喜欢JS的OOP,在学习阶段好像也用不到,总觉得JS的OOP不伦不类的,可能是因为先接触了Java,所以对JS的OO部分有些抵触. 偏见归偏见,既然面 ...
1
js实现的几种继承方式
日志他山之石,可以攻玉,本人一直以谦虚的态度学他人之所长,补自己之所短,望各位老师指正! 拜谢 js几种继承方式,学习中的总结: 所谓的继承是为了继承共有的属性,减少不必要代码的书写 第一种:借用构造函数 ...
1
浅谈js函数三种定义方式 & 四种调用方式 & 调用顺序
日志在Javascript定义一个函数一般有如下三种方式: 函数关键字(function)语句: function fnMethodName(x){alert(x);} 函数字面量(Function Li ...
2
JavaScript几种继承方式
日志我们先构建一个Person的构造函数 function Person(name) { this.name=name; } Person.prototype.sayHi=function () { co ...
2
从jQuery中学习来的另一种继承方式(技巧)
日志   遵从Js的原型链规则,利用js灵活的特性灵活地改造原型,可以实现各种创意地继承方式,昨天研究了jQuery,对作者实现继承的方式感到佩服,他对js原型和原型链的理解比较透彻,运用自如.这里给出j ...
1
JavaScript的3种继承方式
日志JavaScript的继承方式有多种,这里列举3种,分别是原型继承.类继承以及混合继承. 1.原型继承 优点:既继承了父类的模板,又继承了父类的原型对象: 缺点:不是子类实例传参,而是需要通过父类实例 ...
1
C++继承(一) 三种继承方式
日志继承定义 继承是使代码可以复用的重要手段,也是面向对象程序设计的核心思想之一. 继承就是不修改原有的类,直接利用原来的类的属性和方法并进行扩展.原来的类称为基类,继承的类称为派生类,他们的关系就像父子 ...
2
C++中的三种继承方式
日志1,被忽略的细节:     1,冒号( :)表示继承关系,Parent 表示被继承的类,public 的意义是什么? class Parent { }; class Child : public Pa ...
1
JavaScript中的几种继承方式对比
日志转自:http://blog.csdn.net/kkkkkxiaofei/article/details/46474069 从’严格’意义上说,JavaScript并不是一门真正的面向对象语言.这种说 ...
1
[UE4]C++三种继承方式
日志(1) 公有继承(public) 公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问. (2)私有继承(pri ...
1
JavaScript_几种继承方式(2017-07-04)
日志原型链继承 核心: 将父类的实例作为子类的原型 //父类 function SuperType() { this.property = true; } SuperType.prototype.getS ...
1
ECMAScript有6种继承方式(实现继承)
日志本人对于ECMAScript继承机制有些见解,如果说的不对,敬请赐教~~~~ 继承是OO语言(面向对象)挺好的概念,许多OO语言都支持两种继承方式(接口只继承方法签名.实际继承则继承实际的方法),但是 ...
1
子类继承基类的三种继承方式
日志在C++中,子类继承父类有三种继承方式,分别是:public, protected和private.子类继承的方式不同,那么子类对象访问父类中的参数访问权限也不同. public 方式继承:基类的pr ...
2
C++中的类继承(1) 三种继承方式
日志 继承是使代码可以复用的重要手段,也是面向对象程序设计的核心思想之一.简单的说,继承是指一个对象直接使用另一对象的属性和方法.继承呈现了 面向对象程序设 计的层次结构, 体现了 由简单到复杂的认知过程 ...
3
C++三种继承方式
日志一.三种继承方式 继承方式不同,第一个不同是的是派生类继承基类后,各成员属性发生变化.第二个不同是派生类的对象能访问基类中哪些成员发生变化.表格中红色标注. #include <iostream ...
1
JS中5种经典继承方式
日志继承 JS中继承的概念: 通过[某种方式]让一个对象可以访问到另一个对象中的属性和方法,我们把这种方式称之为继承 并不是所谓的xxx extends yyy 为什么要使用继承? 有些对象会有方法(动作 ...
5
js对象的几种创建方式和js实现继承的方式[转]
日志一.js对象的创建方式 1. 使用Object构造函数来创建一个对象,下面代码创建了一个person对象,并用两种方式打印出了Name的属性值. var person = new Object(); ...
1
js两种定义函数、继承方式及区别
日志一:js两种定义函数的方式及区别 1:函数声明: function sayA() { alert("i am A"); } 2:函数表达式: var sayB = function ...
2