CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛
CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛
CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛

深入JavaScript继承原理-源码交易平台丞旭猿

3-13更新:第一次在知乎发博文就获得这么多喜欢,非常感谢大家。今天发现在知乎桌面端格式会乱掉,所以调整了一些排版,清除了所有由 markdown 的“生成的HTML代码。桌面端由HTML生成的 code(inline) 块偶尔会被包在 pre (block)中,导致格式乱掉,阅读体验极为不佳。
本文首发于个人 Github 和掘金, 欢迎 Issue / Fxxk。

ES6的class语法糖你是否已经用得是否炉火纯青呢?那如果回归到ES5呢?本文,将继续上一篇

篇尾提出的疑问如何用 JavaScript 实现类的继承来展开阐述:

通过本文,你将学到:

  1. 如何用 JavaScript 模拟类中的私有变量;
  2. 了解常见的几种 JavaScript 继承方法,原理及其优缺点;
  3. 实现一个较为 fancy 的 JavaScript 继承方法。

此外,如果你完全明白了文末的终极版继承,你也就懂了这两篇所要讲的核心知识,同时,也能说明你拥有不错的JavaScript基础。


我们来回顾一下ES6 / TypeScript / ES5类的写法以作对比。首先,我们创建一个GithubUser类,它拥有一个login方法,和一个静态方法getPublicServices, 用于获取public的方法列表:

classGithubUser{staticgetPublicServices(){return[login]}constructor(username,password){this.username=usernamethis.password=password}login(){console.log(this.username+要登录Github,密码是+this.password)}}

实际上,ES6这个类的写法有一个弊病,实际上,密码password应该是Github用户一个私有变量,接下来,我们用TypeScript重写一下:

class GithubUser {
    static getPublicServices() {
        return [login]
    }
    public username: string
    private password: string
    constructor(username, password) {
        this.username = username
        this.password = password
    }
    public login(): void {
        console.log(this.username + 要登录Github,密码是 + this.password)
    }
}

如此一来,password就只能在类的内部访问了。

当然,这不是最佳方案,各个类的成员会共享同一份 password,鉴于

两位前辈已经有较为完善的回答,请参考:

好了,问题来了,如果结合原型讲解那一文的知识,来用ES5实现这个类呢?just show you my code:

functionGithubUser(username,password){// private属性let_password=password// public属性this.username=username// public方法GithubUser.prototype.login=function(){console.log(this.username+要登录Github,密码是+_password)}}// 静态方法GithubUser.getPublicServices=function(){return[login]}
值得注意的是,我们一般都会把共有方法放在类的原型上,而不会采用this.login = function() {}这种写法。因为只有这样,才能让多个实例引用同一个共有方法,从而避免重复创建方法的浪费。

是不是很直观!留下2个疑问:

  1. 如何实现private方法呢?
  2. 能否实现protected属性/方法呢?

继承

用掘金的用户都应该知道,我们可以选择直接使用 Github 登录,那么,结合上一节,我们如果创建了一个 JuejinUser 来继承 GithubUser,那么 JuejinUser 及其实例就可以调用 Github 的 login 方法了。首先,先写出这个简单 JuejinUser 类:

functionJuejinUser(username,password){// TODO need implementationthis.articles=3// 文章数量JuejinUser.prototype.readArticle=function(){console.log(Read article)}}

由于 ES6/TS 的继承太过直观,本节将忽略。首先概述一下本文将要讲解的几种继承方法:

看起来很多,我们一一论述。


类式继承

因为我们已经得知:

若通过 new Parent() 创建了 Child, 则 Child.__proto__ = Parent.prototype,而原型链则是顺着 __proto__ 依次向上查找。因此,可以通过修改子类的原型为父类的实例来实现继承。

第一直觉的实现如下:

functionGithubUser(username,password){let_password=passwordthis.username=usernameGithubUser.prototype.login=function(){console.log(this.username+要登录Github,密码是+_password)}}functionJuejinUser(username,password){this.articles=3// 文章数量JuejinUser.prototype=newGithubUser(username,password)JuejinUser.prototype.readArticle=function(){console.log(Read article)}}constjuejinUser1=newJuejinUser(ulivz,xxx,3)console.log(juejinUser1)

在浏览器中查看原型链:

诶,不对啊,很明显 juejinUser1.__proto__ 并不是 GithubUser 的一个实例。

实际上,这是因为之前我们为了能够在类的方法中读取私有变量,将 JuejinUser.prototype 的重新赋值放在了构造函数中,而此时实例已经创建,其 __proto__ 还还指向老的JuejinUser.prototype。所以,重新赋值一下实例的 __proto__ 就可以解决这个问题:

functionGithubUser(username,password){let_password=passwordthis.username=usernameGithubUser.prototype.login=function(){console.log(this.username+要登录Github,密码是+_password)}}functionJuejinUser(username,password){this.articles=3// 文章数量constprototype=newGithubUser(username,password)// JuejinUser.prototype = prototype // 这一行已经没有意义了prototype.readArticle=function(){console.log(Read article)}this.__proto__=prototype}constjuejinUser1=newJuejinUser(ulivz,xxx,3)console.log(juejinUser1)

接着查看原型链:

Perfect!原型链已经出来,问题好像得到了完美解决!但实际上还是有明显的问题:

  1. 在原型链上创建了属性(个人觉得这不是一种好的实践,特邀请老师指点。)
  2. 私自篡改__proto__,导致 juejinUser1.__proto__ === JuejinUser.prototype 不成立!从而导致 juejinUser1 instanceof JuejinUser 也不成立 。这不应该发生!

细心的同学会发现,造成这种问题的根本原因在于我们在实例化的时候动态修改了原型,那有没有一种方法可以在实例化之前就固定好类的原型的refernce呢?

事实上,我们可以考虑把类的原型的赋值挪出来:

functionJuejinUser(username,password){this.articles=3// 文章数量}// 此时构造函数还未运行,无法访问 username 和 password !!JuejinUser.prototype=newGithubUser()prototype.readArticle=function(){console.log(Read article)}

但是这样做又有更明显的缺点:

  1. 父类过早地被创建,导致无法接受子类的动态参数;
  2. 仍然在原型上创建了属性,此时,多个子类的实例将共享同一个父类的属性,完蛋, 会互相影响!

举例说明缺点2:

functionGithubUser(username){this.username=Unknown}functionJuejinUser(username,password){}JuejinUser.prototype=newGithubUser()constjuejinUser1=newJuejinUser(ulivz,xxx,3)constjuejinUser2=newJuejinUser(egoist,xxx,0)//  这就是把属性定义在原型链上的致命缺点,你可以直接访问,但修改就是一件难事了!console.log(juejinUser1.username)// UnknownjuejinUser1.__proto__.username=Uconsole.log(juejinUser1.username)// U// 卧槽,无情地影响了另一个实例!!!console.log(juejinUser2.username)// U

由此可见,类式继承的两种方式缺陷太多!


构造函数式继承

通过 call() 来实现继承 (相应的, 你也可以用apply):

functionGithubUser(username,password){let_password=passwordthis.username=usernameGithubUser.prototype.login=function(){console.log(this.username+要登录Github,密码是+_password)}}functionJuejinUser(username,password){GithubUser.call(this,username,password)this.articles=3// 文章数量}constjuejinUser1=newJuejinUser(ulivz,xxx)console.log(juejinUser1.username)// ulivzconsole.log(juejinUser1.username)// xxxconsole.log(juejinUser1.login())// TypeError: juejinUser1.login is not a function

当然,如果继承真地如此简单,那么本文就没有存在的必要了,本继承方法也存在明显的缺陷——构造函数式继承并没有继承父类原型上的方法。


组合式继承

既然上述两种方法各有缺点,但是又各有所长,那么我们是否可以将其结合起来使用呢?没错,这种继承方式就叫做——组合式继承:

functionGithubUser(username,password){let_password=passwordthis.username=usernameGithubUser.prototype.login=function(){console.log(this.username+要登录Github,密码是+_password)}}functionJuejinUser(username,password){GithubUser.call(this,username,password)// 第二次执行 GithubUser 的构造函数this.articles=3// 文章数量}JuejinUser.prototype=newGithubUser();// 第二次执行 GithubUser 的构造函数constjuejinUser1=newJuejinUser(ulivz,xxx)

虽然这种方式弥补了上述两种方式的一些缺陷,但有些问题仍然存在:

  1. 子类仍旧无法传递动态参数给父类!
  2. 父类的构造函数被调用了两次。

本方法很明显执行了两次父类的构造函数,因此,这也不是我们最终想要的继承方式。


原型继承

原型继承实际上是对类式继承的一种封装,只不过其独特之处在于,定义了一个干净的中间类,如下:

functioncreateObject(o){// 创建临时类functionf(){}// 修改类的原型为o, 于是f的实例都将继承o上的方法f.prototype=oreturnnewf()}

熟悉ES5的同学,会注意到,这不就是Object.create吗?没错,你可以认为是如此。

既然只是类式继承的一种封装,其使用方式自然如下:

JuejinUser.prototype=createObject(GithubUser)

也就仍然没有解决类式继承的一些问题。

PS:我个人觉得原型继承和类式继承应该直接归为一种继承!但无赖众多JavaScript书籍均是如此命名,算是follow legacy的标准吧。

寄生继承

寄生继承是依托于一个对象而生的一种继承方式,因此称之为寄生。

constjuejinUserSample={username:ulivz,password:xxx}functionJuejinUser(obj){varo=Object.create(obj)o.prototype.readArticle=function(){console.log(Read article)}returno;}varmyComputer=newCreateComputer(computer);

由于实际生产中,继承一个单例对象的场景实在是太少,因此,我们仍然没有找到最佳的继承方法。


寄生组合式继承

听名字看起来很玄乎,先上代码:

// 寄生组合式继承的核心方法functioninherit(child,parent){// 继承父类的原型constp=Object.create(parent.prototype)// 重写子类的原型child.prototype=p// 重写被污染的子类的constructorp.constructor=child}// GithubUser, 父类functionGithubUser(username,password){let_password=passwordthis.username=username}GithubUser.prototype.login=function(){console.log(this.username+要登录Github,密码是+_password)}// GithubUser, 子类functionJuejinUser(username,password){GithubUser.call(this,username,password)// 继承属性this.articles=3// 文章数量}// 实现原型上的方法inherit(JuejinUser,GithubUser)// 在原型上添加新方法JuejinUser.prototype.readArticle=function(){console.log(Read article)}constjuejinUser1=newJuejinUser(ulivz,xxx)console.log(juejinUser1)

来浏览器中查看结果:

简单说明一下:

  • 子类继承了父类的属性和方法,同时,属性没有被创建在原型链上,因此多个子类不会共享同一个属性。
  • 子类可以传递动态参数给父类!
  • 父类的构造函数只执行了一次!

Nice!这才是我们想要的继承方法。然而,仍然存在一个美中不足的问题:

  • 子类想要在原型上添加方法,必须在继承之后添加,否则将覆盖掉原有原型上的方法。这样的话 若是已经存在的两个类,就不好办了。

所以,我们可以将其优化一下:

functioninherit(child,parent){// 继承父类的原型constparentPrototype=Object.create(parent.prototype)// 将父类原型和子类原型合并,并赋值给子类的原型child.prototype=Object.assign(parentPrototype,child.prototype)// 重写被污染的子类的constructorp.constructor=child}

但实际上,使用 Object.assign 来进行 copy 仍然不是最好的方法,根据

的描述:

The Object.assign() method is used to copy the values of all enumerable own properties from one or more source objects to a target object. It will return the target object.

其中有个很关键的词:enumerable,这已经不是本节讨论的知识了,不熟悉的同学可以参考 :

补习。简答来说,上述的继承方法只适用于 copy 原型链上可枚举的方法,此外,如果子类本身已经继承自某个类,以上的继承将不能满足要求。


终极版继承

为了让代码更清晰,我用ES6的一些API,写出了这个我所认为的最合理的继承方法:

  1. 用 Reflect 代替了Object;
  2. 用 Reflect.getPrototypeOf 来代替ob.__ptoto__;
  3. 用 Reflect.ownKeys 来读取所有可枚举/不可枚举/Symbol的属性;
  4. 用 Reflect.getOwnPropertyDescriptor 读取属性描述符;
  5. 用 Reflect.setPrototypeOf 来设置__ptoto__。

源代码如下:

/*!* fancy-inherit* (c) 2016-2018 ULIVZ*/// 不同于object.assign, 该 merge方法会复制所有的源键// 不管键名是 Symbol 或字符串,也不管是否可枚举functionfancyShadowMerge(target,source){for(constkeyofReflect.ownKeys(source)){Reflect.defineProperty(target,key,Reflect.getOwnPropertyDescriptor(source,key))}returntarget}// Corefunctioninherit(child,parent){constobjectPrototype=Object.prototype// 继承父类的原型constparentPrototype=Object.create(parent.prototype)letchildPrototype=child.prototype// 若子类没有继承任何类,直接合并子类原型和父类原型上的所有方法// 包含可枚举/不可枚举的方法if(Reflect.getPrototypeOf(childPrototype)===objectPrototype){child.prototype=fancyShadowMerge(parentPrototype,childPrototype)}else{// 若子类已经继承子某个类// 父类的原型将在子类原型链的尽头补全while(Reflect.getPrototypeOf(childPrototype)!==objectPrototype){childPrototype=Reflect.getPrototypeOf(childPrototype)}Reflect.setPrototypeOf(childPrototype,parent.prototype)}// 重写被污染的子类的constructorparentPrototype.constructor=child}

测试:

// GithubUserfunctionGithubUser(username,password){let_password=passwordthis.username=username}GithubUser.prototype.login=function(){console.log(this.username+要登录Github,密码是+_password)}// JuejinUserfunctionJuejinUser(username,password){GithubUser.call(this,username,password)WeiboUser.call(this,username,password)this.articles=3}JuejinUser.prototype.readArticle=function(){console.log(Read article)}// WeiboUserfunctionWeiboUser(username,password){this.key=username+password}WeiboUser.prototype.compose=function(){console.log(compose)}// 先让 JuejinUser 继承 GithubUser,然后就可以用github登录掘金了inherit(JuejinUser,GithubUser)// 再让 JuejinUser 继承 WeiboUser,然后就可以用weibo登录掘金了inherit(JuejinUser,WeiboUser)constjuejinUser1=newJuejinUser(ulivz,xxx)console.log(juejinUser1)console.log(juejinUser1instanceofGithubUser)// trueconsole.log(juejinUser1instanceofWeiboUser)// true

最后用一个问题来检验你对本文的理解:

  • 改写上述继承方法,让其支持 inherit(A, B, C …), 实现类A依次继承后面所有的类,但除了A以外的类不产生继承关系。

总结

  • 我们可以使用function来模拟一个类;
  • JavaScript类的继承是基于原型的, 一个完善的继承方法,其继承过程是相当复杂的;
  • 虽然建议实际生产中直接使用ES6的继承,但仍建议深入了解内部继承机制。

题外话

最后放一个彩蛋,为什么我会在寄生组合式继承中尤其强调enumerable这个属性描述符呢,因为:

在ES6中,默认所有类的方法是不可枚举的!

以上,全文终。)

声明:本文部分素材转载自互联网,如有侵权立即删除 。

© 版权声明
THE END
喜欢就支持一下吧
点赞0赞赏 分享
相关推荐
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容