背景介绍JavaScript是编程语言世界中的一种特殊类型。它与其他编程语言有很大不同。JavaScript可以在运行时动态改变变量的类型。比如isTimeout这样的变量,你永远想象不到有多少种类型。除了布尔值true和false之外,还可能是undefined,1和0,时间戳,甚至是对象。如果代码运行异常,打开浏览器,开始断点调试,发现变量InfoList在第一次赋值时是一个数组:[{name:'test1',value:'11'},{name:'test2',value:'22'}]一段时间后变成对象:{test1:'11',test2:'22'}除了运行时可以赋值给任意类型的变量外,继承也可以用JavaScript实现,但它不是像Java、C++、C#编程语言那样实现基于类的继承,而是基于原型的继承。这是因为JavaScript中有一个特殊的存在:对象。每个对象还有一个原型对象,它可以从中继承方法和属性。当谈到对象和原型时,有以下问题:JavaScript函数如何成为对象?proto和prototype是什么关系?JavaScript中的对象是如何实现继承的?JavaScript如何访问对象的方法和属性?原型对象和对象在JavaScript中,对象由一组或多组属性和值组成:{key1:value1,key2:value2,key3:value3,}在JavaScript中,对象非常通用,因为它的值可以是基本类型(number,string,boolean,null,undefined,bigint,andsymbol),也可以是对象和函数。无论是对象、函数还是数组,都是Object的实例,也就是说在JavaScript中,除了原始类型,其余都是对象。这也回答了问题1:JavaScript函数如何成为对象?在JavaScript中,函数也是一个特殊的对象,它也有属性和值。所有的函数都会有一个特殊的属性prototype,这个属性的值就是一个对象,也就是我们常说的“原型对象”。我们可以在控制台打印这个属性:functionPerson(name){this.name=name;}console.log(Person.prototype);打印结果显示为:可以看到,prototype对象有两个属性:constructor和proto。至此,我们似乎看到了“2:proto与prototype是什么关系?”这个问题的答案。在JavaScript中,proto属性指向对象的原型对象。对于函数来说,它的原型对象就是prototype。函数的原型对象prototype具有以下特点:默认情况下,所有函数的原型对象(prototype)都有一个constructor属性,指向关联的构造函数,这里的构造函数就是Person函数;Person函数的原型对象(prototype)也有自己的原型对象,用proto属性表示。前面说过,函数是Object的一个实例,所以Person.prototype的原型对象就是Object.prototype。我们可以用这样一个图来描述prototype、proto、constructor这三个属性之间的关系:从这个图中,我们可以发现这样一个关系:在JavaScript中,proto属性指向对象的prototype对象;对于函数,每个函数都有一个prototype属性,即函数的原型对象;之所以使用prototype和proto来实现继承对象被广泛使用,是因为对象的属性值可以是任何类型。因此,该属性的值也可以是另一个对象,这意味着JavaScript可以这样做:通过将对象A的proto属性赋值给对象B,即:A.__proto__=B这时候,A.proto就可以了用于访问B的属性和方法。通过这种方式,JavaScript可以在两个对象之间建立关联,使一个对象可以访问另一个对象的属性和方法,从而实现继承;使用prototype和proto实现继承以Person为例,当我们使用newPerson()创建对象时,JavaScript会创建一个构造函数Person的实例。例如这里我们创建了一个名为“zhangsan”的Person:varzhangsan=newPerson("zhangsan");prototype对象prototype赋值给实例对象zhangsan的proto属性,实现zhangsan对Person的继承,即执行如下代码://JavaScript引擎执行如下代码varzhangsan={};zhangsan.__proto__=Person.prototype;Person.call(zhangsan,"zhangsan");我们打印一下zhangsan实例:console.log(zhangsan)结果如下图所示:可以看到,zhangsan是Person的一个实例对象,它的proto指向Person的原型对象,即Person。原型。这时候我们就加上上图的关系:从这个图中我们可以很清楚的看到constructor与constructor属性、原型对象(prototype)与proto、instance对象的关系,这样就容易混淆了很多。根据这张图,我们可以得到如下关系:每个函数的原型对象(Person.prototype)都有一个constructor属性,指向原型对象的构造函数(Person);使用构造函数(newPerson())可以创建Object,创建的对象称为实例对象(lily);实例对象通过将proto属性指向构造函数的原型对象(Person.prototype),实现对原型对象的继承。那么现在,关于proto和prototype的关系,我们可以得到这样的答案:每个对象都有一个proto属性来标识它继承的原型对象,但是只有函数才有prototype属性;对于函数,每个函数都有一个原型属性,它是函数的原型对象;通过将实例对象的proto属性赋值给其构造函数的原型对象prototype,JavaScript可以使用构造函数创建对象来实现继承。所以一个对象可以通过proto访问原型对象上的属性和方法,原型也可以通过proto访问它的原型对象,所以我们在实例和原型之间构建了一个原型链。红线表示:通过原型链访问一个对象的方法和属性当JavaScript试图访问一个对象的属性时,它会基于原型链进行搜索。搜索过程如下:首先,首先搜索对象。如果找不到,则向上查找对象的原型对象,对象的原型对象的原型对象等(套娃警告);JavaScript中的所有对象都来自Object,Object.prototype.proto===null。null没有原型,充当原型链中的最后一环;JavaScript会遍历被访问对象的整个原型链,如果最后还是找不到,就会认为该对象的属性值为undefined。我们可以用一个具体的例子来表示基于原型链的对象属性的访问过程。在这个例子中,我们构建对象的原型链并访问属性值:varo={a:1,b:2};//假设我们有一个对象o,它有自己的属性a和b:o.__proto__={b:3,c:4};//o的原型o.__proto__有属性b和c:当我们得到属性值时,会触发原型链的查找:console.log(o.a);//o.a=>1console.log(o.b);//o.b=>2console.log(o.c);//o.c=>o.__proto__.c=>4console.log(o.d);//o.c=>o.__proto__。d=>o.__proto__.__proto__==null=>undefined综上所述,整个原型链如下:{a:1,b:2}--->{b:3,c:4}--->null,//这里是原型链的结尾,也就是null可以看出,当我们对对象进行属性值获取的时候,就会触发对象的原型链查找过程。由于JavaScript是通过遍历原型链来访问对象的属性的,所以我们可以通过原型链进行继承。也就是说,可以通过原型链访问原型对象上的属性和方法,我们不需要在创建对象时为对象重新赋值/添加方法。例如,当我们调用lily.toString()时,JavaScript引擎会执行以下操作:首先检查lily对象是否有可用的toString()方法;如果没有,则``检查百合的原型对象(Person.prototype)是否有toString()方法;如果没有,则检查Person()构造函数的prototype属性指向的对象的prototype对象(即Object.prototype)是否有可用的toString()方法,并调用该方法。由于通过原型链查找属性,需要逐层遍历每个原型对象,这可能会造成性能问题:当试图访问一个不存在的属性时,会遍历整个原型链;在原型链上查找属性非常耗时,对性能有副作用,这在性能关键的情况下很重要。因此,我们在设计对象时,需要注意代码中原型链的长度。当原型链过长时,可以选择分解,避免可能出现的性能问题。其他实现继承的方式除了通过原型链实现JavaScript继承外,JavaScript中实现继承的方式还有经典继承(窃取构造函数)、组合继承、原型继承、寄生继承等。原型链继承方式中引用类型的属性为所有实例共享,实例不能私有;经典的继承方式可以实现实例属性的私有性,但要求类型只能由构造函数定义;组合继承结合了原型链继承和构造函数的优点,其实现如下:functionParent(name){//私有属性,不共享this.name=name;}//定义需要重用和共享的方法关于父类原型Parent.prototype.speak=function(){console.log("hello");};functionChild(name){Parent.call(this,name);}//继承方法Child.prototype=newParent();组合继承模式在父类原型上定义共享属性,通过构造函数分配私有属性的方法实现对象和方法的按需共享,是JavaScript中最常用的继承模式。虽然实现继承的方式有很多种,但实际上都离不开原型对象和原型链的内容。因此,掌握proto、原型、对象继承的知识,是我们实现各种继承方式的前提。小结关于JavaScript中的原型和继承,经常出现在我们的面试题中。随着ES6/ES7等新语法糖的出现,可能更倾向于使用class/extends等语法来编写代码,原型继承等概念逐渐淡出。其次,JavaScript的设计本质上没有改变,继承仍然是基于原型。如果我们不了解这些内容,当我们遇到一些超出我们自身认知的内容时,我们可能很容易束手无策。
