You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
functioninstance_of(L,R){//L 表示左表达式,R 表示右表达式varO=R.prototype;// 取 R 的显示原型L=L.__proto__;// 取 L 的隐式原型while(true){if(L===null)returnfalse;if(O===L)// 这里重点:当 O 严格等于 L 时,返回 true returntrue;L=L.__proto__;}}
functionPerson(name,age){this.name=name;this.age=age;this.sex='male';}Person.prototype.isHandsome=true;Person.prototype.sayName=function(){console.log(`Hello , my name is ${this.name}`);}lethandsomeBoy=newPerson('Nealyang',25);console.log(handsomeBoy.name)// Nealyangconsole.log(handsomeBoy.sex)// maleconsole.log(handsomeBoy.isHandsome)// truehandsomeBoy.sayName();// Hello , my name is Nealyang
使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性
返回 obj
下面我们来测试一下:
functionPerson(name,age){this.name=name;this.age=age;this.sex='male';}Person.prototype.isHandsome=true;Person.prototype.sayName=function(){console.log(`Hello , my name is ${this.name}`);}functionobjectFactory(){letobj=newObject(),//从Object.prototype上克隆一个对象Constructor=[].shift.call(arguments);//取得外部传入的构造器console.log({Constructor})constF=function(){};F.prototype=Constructor.prototype;obj=newF();//指向正确的原型Constructor.apply(obj,arguments);//借用外部传入的构造器给obj设置属性returnobj;//返回 obj};lethandsomeBoy=objectFactory(Person,'Nealyang',25);console.log(handsomeBoy.name)// Nealyangconsole.log(handsomeBoy.sex)// maleconsole.log(handsomeBoy.isHandsome)// truehandsomeBoy.sayName();// Hello , my name is Nealyang
注意上面我们没有直接修改 obj 的__proto__隐式挂载。
new 手写版本二
考虑构造函数又返回值的情况:
如果构造函数返回一个对象,那么我们也返回这个对象
如上否则,就返回默认值
functionobjectFactory(){varobj=newObject(),//从Object.prototype上克隆一个对象Constructor=[].shift.call(arguments);//取得外部传入的构造器varF=function(){};F.prototype=Constructor.prototype;obj=newF();//指向正确的原型varret=Constructor.apply(obj,arguments);//借用外部传入的构造器给obj设置属性returntypeofret==='object' ? ret : obj;//确保构造器总是返回一个对象};
关于 ES6 中的 class 的一些基本用法和介绍,限于篇幅,本文就不做介绍了。该章节,我们主要通过 babel的 REPL来查看分析 es6 中各个语法糖包括继承的一些实现方式。
基础类
我们就会按照这个类,来回摩擦。然后再来分析编译后的代码。
"use strict";function_instanceof(left,right){if(right!=null&&typeofSymbol!=="undefined"&&right[Symbol.hasInstance]){return!!right[Symbol.hasInstance](left);}else{returnleftinstanceofright;}}function_classCallCheck(instance,Constructor){if(!_instanceof(instance,Constructor)){thrownewTypeError("Cannot call a class as a function");}}varPerson=functionPerson(name){_classCallCheck(this,Person);this.name=name;};
"use strict";function_instanceof(left,right){...}function_classCallCheck(instance,Constructor){...}varParent=functionParent(name){...};function_typeof(obj){if(typeofSymbol==="function"&&typeofSymbol.iterator==="symbol"){_typeof=function_typeof(obj){returntypeofobj;};}else{_typeof=function_typeof(obj){returnobj&&typeofSymbol==="function"&&obj.constructor===Symbol&&obj!==Symbol.prototype ? "symbol" : typeofobj;};}return_typeof(obj);}function_possibleConstructorReturn(self,call){if(call&&(_typeof(call)==="object"||typeofcall==="function")){returncall;}return_assertThisInitialized(self);}function_assertThisInitialized(self){if(self===void0){thrownewReferenceError("this hasn't been initialised - super() hasn't been called");}returnself;}function_getPrototypeOf(o){_getPrototypeOf=Object.setPrototypeOf ? Object.getPrototypeOf : function_getPrototypeOf(o){returno.__proto__||Object.getPrototypeOf(o);};return_getPrototypeOf(o);}function_inherits(subClass,superClass){if(typeofsuperClass!=="function"&&superClass!==null){thrownewTypeError("Super expression must either be null or a function");}subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor: {value: subClass,writable: true,configurable: true}});if(superClass)_setPrototypeOf(subClass,superClass);}function_setPrototypeOf(o,p){_setPrototypeOf=Object.setPrototypeOf||function_setPrototypeOf(o,p){o.__proto__=p;returno;};return_setPrototypeOf(o,p);}varChild=/*#__PURE__*/function(_Parent){_inherits(Child,_Parent);functionChild(name,age){var_this;_classCallCheck(this,Child);_this=_possibleConstructorReturn(this,_getPrototypeOf(Child).call(this,name));// 调用父类的 constructor(name)_this.age=age;return_this;}returnChild;}(Parent);varchild1=newChild('全栈前端精选','0.3');console.log(child1);
function_inherits(subClass,superClass){if(typeofsuperClass!=="function"&&superClass!==null){//subClass 类型判断thrownewTypeError("Super expression must either be null or a function");}subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor: {//Object.create 第二个参数是给subClass.prototype添加了 constructor 属性value: subClass,writable: true,configurable: true//注意这里enumerable没有指名,默认是 false,也就是说constructor为不可枚举的。}});if(superClass)_setPrototypeOf(subClass,superClass);}function_setPrototypeOf(o,p){_setPrototypeOf=Object.setPrototypeOf||function_setPrototypeOf(o,p){o.__proto__=p;returno;};return_setPrototypeOf(o,p);}
function_possibleConstructorReturn(self,call){if(call&&(_typeof(call)==="object"||typeofcall==="function")){returncall;}return_assertThisInitialized(self);}function_assertThisInitialized(self){if(self===void0){thrownewReferenceError("this hasn't been initialised - super() hasn't been called");}returnself;}
前言
【THE LAST TIME】一直是我想写的一个系列,旨在厚积薄发,重温前端。
也是给自己的查缺补漏和技术分享。
欢迎大家多多评论指点吐槽。
首先我想说,【THE LAST TIME】系列的的内容,向来都是包括但不限于标题的范围。
再回来说原型,老生常谈的问题了。但是着实 现在不少熟练工也貌似没有梳理清楚
Function
和Object
、prototype
和__proto__
的关系,本文将从原型到继承到 es6 语法糖的实现来介绍系统性的介绍 JavaScript 继承。如果你能够回答上来以下问题,那么这位看官,基本这篇不用再花时间阅读了~typeof
判断null
是Object
类型?Function
和Object
是什么关系?new
关键字具体做了什么?手写实现。prototype
和__proto__
是什么关系?什么情况下相等?extends
关键字实现原理是什么如果对以上问题有那么一些疑惑~那么。。。
THE LAST TIME 系列回顾
目录
__proto__
原型一把梭
这。。。说是最基础没人反驳吧,说没有用有人反驳吧,说很多人到现在没梳理清楚没人反驳吧!OK~ 为什么文章那么多,你却还没有弄明白?
在概念梳理之前,我们还是放一张老掉牙所谓的经典神图:
.__proto__
都是Function.prototype
老规矩,我们直接来梳理概念。
函数对象和普通对象
老话说,万物皆对象。而我们都知道在 JavaScript 中,创建对象有好几种方式,比如对象字面量,或者直接通过构造函数 new 一个对象出来:
暂且我们先不管上面的代码有什么意义。至少,我们能看出,都是对象,却存在着差异性
其实在 JavaScript 中,我们将对象分为函数对象和普通对象。所谓的函数对象,其实就是 JavaScript 的用函数来模拟的类实现。JavaScript 中的 Object 和 Function 就是典型的函数对象。
关于函数对象和普通对象,最直观的感受就是。。。咱直接看代码:
不知道大家看到上述代码有没有一些疑惑的地方~别着急,我们一点一点梳理。
上述代码中,
obj1
,obj2
,obj3
,obj4
都是普通对象,fun1
,fun2
,fun3
都是Function
的实例,也就是函数对象。所以可以看出,所有 Function 的实例都是函数对象,其他的均为普通对象,其中包括 Function 实例的实例。
JavaScript 中万物皆对象,而对象皆出自构造(构造函数)。
上图中,你疑惑的点是不是
Function
和new Function
的关系。其实是这样子的:__proto__
但是在 JavaScript 中,函数也是对象,所以函数也拥有
__proto__
和constructor
属性。结合上面我们介绍的
Object
和Function
的关系,看一下代码和关系图再梳理上图关系之前,我们再来讲解下
__proto__
。__proto__
的例子,说起来比较复杂,可以说是一个历史问题。ECMAScript 规范描述
prototype
是一个隐式引用,但之前的一些浏览器,已经私自实现了__proto__
这个属性,使得可以通过obj.__proto__
这个显式的属性访问,访问到被定义为隐式属性的prototype
。因此,情况是这样的,ECMAScript 规范说
prototype
应当是一个隐式引用:Object.getPrototypeOf(obj)
间接访问指定对象的prototype
对象Object.setPrototypeOf(obj, anotherObj)
间接设置指定对象的prototype
对象__proto__
的口子,使得可以通过obj.__proto__
直接访问原型,通过obj.__proto__ = anotherObj
直接设置原型__proto__
属性纳入了规范的一部分从浏览器的打印结果我们可以看出,上图对象
a
存在一个__proto__
属性。而事实上,他只是开发者工具方便开发者查看原型的故意渲染出来的一个虚拟节点。虽然我们可以查看,但实则并不存在该对象上。__proto__
属性既不能被for in
遍历出来,也不能被Object.keys(obj)
查找出来。访问对象的
obj.__proto__
属性,默认走的是Object.prototype
对象上__proto__
属性的 get/set 方法。关于更多
__proto__
更深入的介绍,可以参看工业聚大佬的《深入理解 JavaScript 原型》一文。这里我们需要知道的是,
__proto__
是对象所独有的,并且__proto__
是一个对象指向另一个对象,也就是他的原型对象。我们也可以理解为父类对象。它的作用就是当你在访问一个对象属性的时候,如果该对象内部不存在这个属性,那么就回去它的__proto__
属性所指向的对象(父类对象)上查找,如果父类对象依旧不存在这个属性,那么就回去其父类的__proto__
属性所指向的父类的父类上去查找。以此类推,知道找到null
。而这个查找的过程,也就构成了我们常说的原型链。prototype
在规范里,prototype 被定义为:给其它对象提供共享属性的对象。
prototype
自己也是对象,只是被用以承担某个职能罢了.所有对象,都可以作为另一个对象的
prototype
来用。修改
__proto__
的关系图,我们添加了prototype
,prototype
是函数所独有的。**它的作用就是包含可以给特定类型的所有实例提供共享的属性和方法。它的含义就是函数的远行对象,**也就是这个函数所创建的实例的远行对象,正如上图:nealyang.__proto__ === Person.prototype
。 任何函数在创建的时候,都会默认给该函数添加prototype
属性.constructor
constructor
属性也是对象所独有的,它是一个对象指向一个函数,这个函数就是该对象的构造函数。注意,每一个对象都有其对应的构造函数,本身或者继承而来。单从
constructor
这个属性来讲,只有prototype
对象才有。每个函数在创建的时候,JavaScript 会同时创建一个该函数对应的prototype
对象,而函数创建的对象.__proto__ === 该函数.prototype
,该函数.prototype.constructor===该函数本身
,故通过函数创建的对象即使自己没有constructor
属性,它也能通过__proto__
找到对应的constructor
,所以任何对象最终都可以找到其对应的构造函数。唯一特殊的可能就是我开篇抛出来的一个问题。JavaScript 原型的老祖宗:
Function
。它是它自己的构造函数。所以Function.prototype === Function.__proto
。为了直观了解,我们在上面的图中,继续添加上
constructor
:其中
constructor
属性,虚线表示继承而来的 constructor 属性。typeof && instanceof 原理
问什么好端端的说原型、说继承会扯到类型判断的原理上来呢。毕竟原理上有一丝的联系,往往面试也是由浅入深、顺藤摸瓜的拧出整个知识面。所以这里我们也简单说一下吧。
typeof
基本用法
typeof
的用法想必大家都比较熟悉,一般被用于来判断一个变量的类型。我们可以使用typeof
来判断number
、undefined
、symbol
、string
、function
、boolean
、object
这七种数据类型。但是遗憾的是,typeof
在判断object
类型时候,有些许的尴尬。它并不能明确的告诉你,该object
属于哪一种object
。原理浅析
要想弄明白为什么
typeof
判断null
为object
,其实需要从js 底层如何存储变量类型来说其。虽然说,这是 JavaScript 设计的一个 bug。在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于
null
代表的是空指针(大多数平台下值为 0x00),因此,null
的类型标签是 0,typeof null
也因此返回"object"
。曾有一个 ECMAScript 的修复提案(通过选择性加入的方式),但被拒绝了。该提案会导致typeof null === 'null'
。js 在底层存储变量的时候,会在变量的机器码的低位1-3位存储其类型信息:
但是,对于
undefined
和null
来说,这两个值的信息存储是有点特殊的:null
:所有机器码均为0undefined
:用 −2^30 整数来表示所以在用
typeof
来判断变量类型的时候,我们需要注意,最好是用typeof
来判断基本数据类型(包括symbol
),避免对null
的判断。instanceof
instanceof
和typeof
非常的类似。instanceof
运算符用来检测constructor.prototype
是否存在于参数object
的原型链上。与typeof
方法不同的是,instanceof
方法要求开发者明确地确认对象为某特定类型。基本用法
如上,是
instanceof
的基本用法,它可以判断一个实例是否是其父类型或者祖先类型的实例。为什么
Object
和Function
instanceof
自己等于true
,而其他类instanceof
自己却又不等于true
呢?如何解释?要想从根本上了解
instanceof
的奥秘,需要从两个方面着手:1,语言规范中是如何定义这个运算符的。2,JavaScript 原型继承机制。原理浅析
经过上述的分析,相比大家对这种经典神图已经不那么陌生了吧,那咱就对着这张图来聊聊
instanceof
这里,我直接将规范定义翻译为 JavaScript 代码如下:
所以如上原理,加上上文解释的原型相关知识,我们再来解析下为什么
Object
和Function
instanceof
自己等于true
。Object instanceof Object
Foo instanceof Foo
ES5 中的继承实现方式
在继承实现上,工业聚大大在他的原型文章中,将原型继承分为两大类,显式继承和隐式继承。感兴趣的可以点击文末参考链接查看。
但是本文还是希望能够基于“通俗”的方式来讲解几种常见的继承方式和优缺点。大家可多多对比查看,其实原理都是一样,名词也只是所谓的代称而已。
new 关键字
在讲解继承之前呢,我觉得
new
这个东西很有必要介绍下~一个例子看下
new
关键字都干了啥从上面的例子我们可以看到:
Person
构造函数里的属性Person.prototype
中的属性new 手写版本一
new Object()
的方式新建了一个对象 objarguments
会被去除第一个参数apply
,改变构造函数this
的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性下面我们来测试一下:
注意上面我们没有直接修改 obj 的
__proto__
隐式挂载。new 手写版本二
考虑构造函数又返回值的情况:
类式继承
虽然实现起来清晰简洁,但是这种继承方式有两个缺点:
构造函数继承
SuperClass.call(this,id)
当然就是构造函数继承的核心语句了.由于父类中给this绑定属性,因此子类自然也就继承父类的共有属性。由于这种类型的继承没有涉及到原型prototype
,所以父类的原型方法自然不会被子类继承,而如果想被子类继承,就必须放到构造函数中,这样创建出来的每一个实例都会单独的拥有一份而不能共用,这样就违背了代码复用的原则,所以综合上述两种,我们提出了组合式继承方法组合式继承
如上,我们就解决了之前说到的一些问题,但是是不是从代码看,还是有些不爽呢?至少这个
SuperClass
的构造函数执行了两遍就感觉非常的不妥.原型式继承
原型式继承大致的实现方式如上,是不是想到了我们
new
关键字模拟的实现?其实这种方式和类式继承非常的相似,他只是对类式继承的一个封装,其中的过渡对象就相当于类式继承的子类,只不过在原型继承中作为一个普通的过渡对象存在,目的是为了创建要返回的新的实例对象。
如上代码我们可以看出,原型式继承和类式继承一个样子,对于引用类型的变量,还是存在子类实例共享的情况。
所以,我们还有下面的寄生式继
寄生式继承
其实寄生式继承就是对原型继承的拓展,一个二次封装的过程,这样新创建的对象不仅仅有父类的属性和方法,还新增了别的属性和方法。
寄生组合式继承
回到之前的组合式继承,那时候我们将类式继承和构造函数继承组合使用,但是存在的问题就是子类不是父类的实例,而子类的原型是父类的实例,所以才有了寄生组合式继承
而寄生组合式继承是寄生式继承和构造函数继承的组合。但是这里寄生式继承有些特殊,这里他处理不是对象,而是类的原型。
组合式继承中,通过构造函数继承的属性和方法都是没有问题的,所以这里我们主要探究通过寄生式继承重新继承父类的原型。
我们需要继承的仅仅是父类的原型,不用去调用父类的构造函数。换句话说,在构造函数继承中,我们已经调用了父类的构造函数。因此我们需要的就是父类的原型对象的一个副本,而这个副本我们可以通过原型继承拿到,但是这么直接赋值给子类会有问题,因为对父类原型对象复制得到的复制对象p中的
constructor
属性指向的不是subClass
子类对象,因此在寄生式继承中要对复制对象p做一次增强,修复起constructor
属性指向性不正确的问题,最后将得到的复制对象p赋值给子类原型,这样子类的原型就继承了父类的原型并且没有执行父类的构造函数。这种方式继承其实如上图所示,其中最大的改变就是子类原型中的处理,被赋予父类原型中的一个引用,这是一个对象,因此有一点你需要注意,就是子类在想添加原型方法必须通过prototype.来添加,否则直接赋予对象就会覆盖从父类原型继承的对象了.
ES6 类的实现原理
基础类
_instanceof
就是来判断实例关系的的。上述代码就比较简单了,_classCallCheck
的作用就是检查Person
这个类,是否是通过new
关键字调用的。毕竟被编译成 ES5 以后,function
可以直接调用,但是如果直接调用的话,this
就指向window
对象,就会Throw Error
了.添加属性
其实就是讲属性赋值给谁的问题。如果是实例属性,直接赋值到
this
上,如果是静态属性,则赋值类上。_defineProperty
也就是来判断下是否属性名重复而已。添加方法
看起来代码量还不少,其实就是一个
_createClass
函数和_defineProperties
函数而已。首先看
_createClass
这个函数的三个参数,第一个是构造函数,第二个是需要添加到原型上的函数数组,第三个是添加到类本身的函数数组。其实这个函数的作用非常的简单。就是加强一下构造函数,所谓的加强构造函数就是给构造函数或者其原型上添加一些函数。而
_defineProperties
就是多个_defineProperty
(感觉是废话,不过的确如此)。默认enumerable
为false
,configurable
为true
。其实如上就是 es6 class 的实现原理。
extend 关键字
删去类相关的代码生成,剩下的就是继承的语法糖剖析了。其中
super
关键字表示父类的构造函数,相当于 ES5 的Parent.call(this)
,然后再根据我们上文说到的继承方式,有没有感觉该集成的实现跟我们说的寄生组合式继承非常的相似呢?在 ES6 class 中,子类必须在
constructor
方法中调用super
方法,否则新建实例时会报错。这是因为子类没有自己的this
对象,而是继承父类的this
对象,然后对其进行加工。如果不调用super
方法,子类就得不到this
对象。也正是因为这个原因,在子类的构造函数中,只有调用
super
之后,才可以使用this
关键字,否则会报错。关于 ES6 中原型链示意图可以参照如下示意图:
关于ES6 中的
extend
关键字,上述代码我们完全可以根据执行来看。其实重点代码无非就两行:我们分别来分析下具体的实现:
_inherits
_possibleConstructorReturn
根据上图我们整理的 es6 原型图可知:
所以上面的代码我们可以翻译为:
然后我们再一层一层拨源码的实现
上述代码,
self
其实就是 Child 的 IIFE返回的function
new
调用的this
,打印出来结果如下:这里可能对
Parent.call(this,name)
有些疑惑,没关系,我们可以在 Chrome 下调试下。可以看到,当我们
Parent
的构造函数这么写那么最终,传递给
_possibleConstructorReturn
函数的第二参数call
就是一个undefined
。所以在_possibleConstructorReturn
函数里面会对call
进行判断,返回正确的this
指向:Child
。所以整体代码的目的就是根据 Parent 构造函数的返回值类型确定子类构造函数 this 的初始值 _this。
最后
【THE LAST TIME】系列关于 JavaScript 基础的文章目前更新三篇,我们最后再来一道经典的面试题吧!
老铁,评论区留下你的思考吧~
参考文献
The text was updated successfully, but these errors were encountered: