IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> JavaScript继承的实现方式:原型语言对象继承对象原理剖析 -> 正文阅读

[JavaScript知识库]JavaScript继承的实现方式:原型语言对象继承对象原理剖析

面向对象编程:继承、封装、多态。

对象的继承:A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。这对于代码的复用是非常有用的。

在经典的面向对象语言中,您可能倾向于定义类对象,然后您可以简单地定义哪些类继承哪些类(参考C++ inheritance里的一些简单的例子),JavaScript使用了另一套实现方式,继承的对象函数并不是通过复制而来,而是通过原型链继承

回顾《再谈javascriptjs原型与原型链及继承相关问题

什么是原型语言?

  1. 只有对象,没有类;对象继承对象,而不是类继承类。?

  2. “原型对象”是核心概念。原型对象是新对象的模板,它将自身的属性共享给新对象。一个对象不但可以享有自己创建时和运行时定义的属性,而且可以享有原型对象的属性。?

  3. 每一个对象都有自己的原型对象,所有对象构成一个树状的层级系统。root节点的顶层对象是一个语言原生的对象,只有它没有原型对象,其他所有对象都直接或间接继承它的属性。?

原型语言创建有两个步骤?

  1. 使用”原型对象”作为”模板”生成新对象?:这个步骤是必要的,这是每个对象出生的唯一方式。以原型为模板创建对象,这也是”原型”(prototype)的原意。?

  2. 初始化内部属性?:这一步骤不是必要的。通俗点说,就是,对”复制品”不满意,我们可以”再加工”,使之获得不同于”模板”的”个性”。?

所以在JavaScript的世界里,万物皆对象这个概念从一而终。

JavaScript里面没有类这个概念,es6中class虽然很像类,但实际上只是es5上语法糖而已

function?People(name){
??//属性
??this.name??=?name?||?Annie
??//实例方法
??this.sleep=function(){
????console.log(this.name?+?'正在睡觉')
??}
}
//原型方法
People.prototype.eat?=?function(food){
??console.log(this.name?+?'正在吃:'?+?food);
}

JavaScript的基础方式,首推的就是原型继承

原型链继承

父类的实例作为子类的原型

function?Woman(){?
????this.name=?"SubType";?//?子类属性
}
//?如果此处有Woman的原型对象上的方法,由于原型重定向,下面的代码会覆盖此方法
Woman.prototype=?new?People();//?重写原型对象,代之以一个新类型的实例
//?这里实例化一个?People时,?实际上执行了两步
//?1,新创建的对象复制了父类构造函数内的所有属性及方法
//?2,并将原型?__proto__?指向了父类的原型对象
Woman.prototype.name?=?'haixia';//子原型的属性
Woman.prototype.name?=?()=>{};//子原型方法
let?womanObj?=?new?Woman();

原型链继承优点

  • 父类新增原型方法/原型属性,子类都能访问到

  • 非常纯粹的继承关系,实例是子类的实例,也是父类的实例

  • 简单易于实现

原型链继承缺点:

  1. 可以在子类中增加实例属性,如果要新增加原型属性和方法需要在new 父类构造函数的后面

  2. 无法实现多继承

  3. 来自原型对象的所有属性被所有实例共享,子类可以重写父类原型上的方法

  4. 创建子类实例时,不能向父类构造函数中传参数

解释原型重定向:?Woman.prototype= new People();

  • 自己开辟的堆内存中没有constructor属性,导致类的原型构造函数缺失(解决:自己手动在堆内存中增加constructor属性)

  • 当原型重定向后,浏览器默认开辟的那个原型堆内存会被释放掉,如果之前已经存储了一些方法或者属性,这些东西都会丢失(所以:内置类的原型不允许重定向到自己开辟的堆内存,因为内置类原型上自带很多属性方法,重定向后都没了,这样是不被允许的)

原型继承经典笔试题

function?Parent?()?{
??this.a?=?1;
??this.b?=?[1,?2,?this.a];
??this.c?=?{demo:?5};
??this.show?=?function?()?{
????console.log(this.a?+?'?'?+?this.c.demo?+?':'?+?this.b?+?'\n');
??};
}

function?Child?()?{
??this.a?=?2;
??this.change?=?function?()?{
????this.b.push(this.a);
????this.a?=?this.b.length;
????this.c.demo?=?this.a++;
??};
}

Child.prototype?=?new?Parent();
var?parent?=?new?Parent();
var?child1?=?new?Child();
var?child2?=?new?Child();
child1.a?=?11;
child2.a?=?12;
child1.change();
child2.change();
parent.show();
child1.show();
child2.show();

思考原型公用问题

经典继承

function?extendObj(obj)?{
??if?(Object.create)?{
????return?Object.create(obj)
??}?else?{
????function?F?()?{?}?
????F.prototype?=?obj;?
????return?new?F()
??}
}
var?obj?=?{?name:?"smd",?age:?26,?sayHi:?function?()?{?}?}
var?newObj?=?createObj(obj)
/*?Extend?Function?*/??
function?extend(subClass,superClass){??
????var?Func?=?function(){}?;??
????Func.prototype?=?superClass.prototype?;??
????subClass.prototype?=?new?Func()?;??
????subClass.prototype.constructor?=?subClass?;??
}?;

newObj继承了obj的属性和方法,但是同样出现了共享父类中引用类型属性的问题

实例继承(原型式继承)

function?Wonman(name){
??let?instance?=?new?People();
??instance.name?=?name?||?'wangxiaoxia';
??return?instance;
}
let?wonmanObj?=?new?Wonman();

//?父类
function?People?(name)?{
??this.colors?=?["red",?"blue",?"green"];
??this.name?=?name;?//?父类属性
}
People.prototype.sayName?=?function?()?{?//?父类原型方法
??return?this.name;
};

/**?第一步?*/
//?子类,通过?call?继承父类的实例属性和方法,不能继承原型属性/方法
function?Woman?(name,?subName)?{
??People.call(this,?name);?//?调用?People?的构造函数,并向其传参?
??this.subName?=?subName;
}

/**?第二步?*/
//?解决?call?无法继承父类原型属性/方法的问题
//?Object.create?方法接受传入一个作为新创建对象的原型的对象,创建一个拥有指定原型和若干个指定属性的对象
//?通过这种方法指定的任何属性都会覆盖原型对象上的同名属性
Woman.prototype?=?Object.create(People.prototype,?{
??constructor:?{?//?注意指定?Woman.prototype.constructor?=?Woman
????value:?Woman,
????enumerable:?false,
????writable:?true,
????configurable:?true
??},
??run?:?{
????value:?function(){?//?override
??????People.prototype.run.apply(this,?arguments);
??????//?call?super
??????//?...
????},
????enumerable:?true,
????configurable:?true,
????writable:?true
??}
})

/**?第三步?*/
//?最后:解决?Woman.prototype.constructor?===?People?的问题
//?这里,在上一步已经指定,这里不需要再操作
//Woman.prototype.constructor?=?Woman;

var?instance?=?new?Woman('An',?'sistenAn')

实例继承优点:

  • 不限制调用方式

  • 简单,易实现

实例继承缺点:

  • 不能多次继承

拷贝继承

function?Wonman?(name)?{
??let?instance?=?new?People();
??for?(var?p?in?instance)?{
????Wonman.prototype[p]?=?instance[p];
??}
??Wonman.prototype.name=name||'Tom'
}

let?wonmanObj?=?new?Wonman();

特点:

  • 支持多继承

缺点:

  • 效率较低,内存占用高(因为要拷贝父类的属性)

  • 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)

对象冒充继承

function?Woman(name,?age)?{
??//3行关键代码?此三行用于获取父类的成员及方法
??//用子类的this去冒充父类的this,实现继承
??//父类People中的this.name、sleep,分别成为了子类的成员、子类的方法
??this.method?=?People;
??//接收子类的参数?传给父类
??this.method(name);
??//删除父类
??delete?this.method;

??//此后的this均指子类
??this.age?=?age;
??this.sayWorld?=?function()?{
????alert(age);
??}
}

因为对象冒充的留下,才有call apply的兴起

借用构造函数继承(伪造对象、经典继承)

复制父类的实例属性给子类

  • 函数只不过是在特定环境中执行代码的对象,所以这里使用 apply/call 来实现。

  • 使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

function?Woman(name){
?//继承了People,子类的this传给父类
??People.call(this);?//People.call(this,'zhoulujun');?
??this.name?=?name?||?'andy'
}
let?womanObj?=?new?Woman();

通过这种调用,把父类构造函数的this指向为子类实例化对象引用,从而导致父类执行的时候父类里面的属性都会被挂载到子类的实例上去。

但是通过这种方式,父类原型上的东西是没法继承的,因此函数复用也就无从谈起

Woman无法继承Parent的原型对象,并没有真正的实现继承(部分继承)

借用构造函数继承优点:

  1. 解决了子类构造函数向父类构造函数中传递参数

  2. 解决了子类实例共享父类引用属性的问题

  3. 可以实现多继承(call或者apply多个父类)

借用构造函数继承缺点:

  1. 方法都在构造函数中定义,无法复用

  2. 不能继承原型属性/方法,只能继承父类的实例属性和方法

组合式继承

调用父类构造函数,继承父类的属性,通过将父类实例作为子类原型,实现函数复用

function?Woman(name,age){
??People.call(this,name,age)
}
Woman.prototype?=?new?People();
Woman.prototype.constructor?=?Woman;
let?wonmanObj?=?new?Woman(ren,27);
wonmanObj.eat();

缺点:

  • 由于调用了两次父类,所以产生了两份实例

    • 第一次是 Woman.prototype = new People()

    • 第二次是 在实例化的时候,People.call(this,name,age)

优点:

  • 函数可以复用

  • 不存在引用属性问题

  • 可以继承属性和方法,并且可以继承原型的属性和方法

寄生组合继承

通过寄生的方式来修复组合式继承的不足,完美的实现继承

function?Woman(name,age){
??//继承父类属性
??People.call(this,name,age)
}
//继承父类方法,可以简化为:Woman.prototype?=?Object.create(People.prototype);
(function(){
??//?创建空类
??function?Super?(){};
??Super.prototype?=?People.prototype;
??//父类的实例作为子类的原型
??Woman.prototype?=?new?Super();
})();
//修复构造函数指向问题
Woman.prototype.constructor?=?Woman;
let?womanObj?=?new?Woman();

其实还是有两次执行

es6继承

//class?相当于es5中构造函数
//class中定义方法时,前后不能加function,全部定义在class的protopyte属性中
//class中定义的所有方法是不可枚举的
//class中只能定义方法,不能定义对象,变量等
//class和方法内默认都是严格模式
//es5中constructor为隐式属性
class?People{
??constructor(name='wang',age='27'){
????this.name?=?name;
????this.age?=?age;
??}
??eat(){
????console.log(`${this.name}?${this.age}?eat?food`)
??}
}
//?继承父类属性,super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。
//?子类必须在constructor方法中调用super方法,否则新建实例时会报错。如果子类没有定义constructor方法,这个方法会被默认添加,不管有没有显式定义,任何一个子类都有constructor方法。
class?Woman?extends?People{?
???constructor(name?=?'ren',age?=?'27'){?
?????super(name,?age);?
???}?
????eat(){?
?????//继承父类方法
??????super.eat()?
????}?
}?
let?wonmanObj=new?Woman('xiaoxiami');?
wonmanObj.eat();

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。

ES6 的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。

/**
?*?继承
?*?@param?{*}?subClass?子类
?*?@param?{*}?superClass?父类
?*/
function?_inherits(subClass,?superClass)?{
??//?类型检测
??if?(!superClass?||?typeof?superClass?!==?"function"?)?{
????throw?new?TypeError("Super?expression?must?either?be?null?or?a?function,?not?"?+typeof?superClass);
??}
??/**
???*?Object.create?接受两个参数
???*?指定原型创建对象
???*?@param?{*}?目标原型
???*?@param?{*}?添加属性
???*/
??subClass.prototype?=?Object.create(superClass?&&?superClass.prototype,?{
????constructor:?{
??????value:?subClass,?//?subClass.prototype.constructor?指向?subClass
??????enumerable:?false,?//?constructor?不可枚举
??????writable:?true,
??????configurable:?true
????}
??});

??/**
???*?Object.setPrototypeOf?方法
???*?设置子类的?__proto__?属性指向父类
???*?@param?{*}?子类
???*?@param?{*}?父类
???*/
??if?(superClass)?{
????//?设置子类的__proto__?让?Child?能访问父类静态属性
????Object.setPrototypeOf
????????Object.setPrototypeOf(subClass,?superClass)
??????:?(subClass.__proto__?=?superClass);
??}
}

参考文章:

JavaScript深入之继承的多种方式和优缺点 #16 https://github.com/mqyqingfeng/Blog/issues/16

JavaScript 中的继承 https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects/Inheritance

JavaScript常见的六种继承方式 https://segmentfault.com/a/1190000016708006

js继承的几种方式?https://zhuanlan.zhihu.com/p/37735247

深入浅出js实现继承的7种方式?https://cloud.tencent.com/developer/article/1536957

前端面试必备之JS继承方式总结?https://www.imooc.com/article/20162

转载本站文章《JavaScript继承的实现方式:原型语言对象继承对象原理剖析》,
请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/js6/2015_0520_8494.html

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-08-15 15:25:42  更:2021-08-15 15:27:40 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/26 17:24:25-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计