理解原型每当创建一个函数时,都会按照特定的规则为该函数创建一个prototype属性(指向原型对象)。默认情况下,所有原型对象都会自动获得一个名为constructor的属性,该属性指向与其关联的构造函数。定义构造函数时,原型对象默认只会获取constructor属性,其他所有方法都继承自Object。每次调用构造函数创建新实例时,实例的内部[[Prototype]]指针都会被分配构造函数的原型对象。在脚本中没有标准的方法来访问这个[[Prototype]]属性,但是Firefox/Safari/Chrome在每个对象上公开了一个__proto__属性,通过它可以访问对象的原型。实例与构造函数的原型直接相关,但实例与构造函数之间没有这种关系。不容易形象化,但是你可以通过下面的代码理解原型的行为);console.log(Person.prototype)//如前所述,构造函数有一个原型属性//引用它的原型对象,而这个原型对象有一个构造函数属性,引用这个构造函数//也就是说,两者循环引用console.log(Person.prototype.constructor===Person)//true//正常的原型链会在Object的原型对象处结束//Object原型的原型为nullconsole.log(Person.prototype.__proto__===Object.prototype)//trueconsole.log(Person.prototype.__proto__.constructor===Object)//trueconsole.log(Person.prototype.__proto__.__proto__===null)//trueletperson1=newPerson()letperson2=newPerson()//构造函数、原型对象和实例是3个完全不同的对象console.log(person1!==Person)//trueconsole.log(person1!==Person.prototype)//trueconsole.log(Person.prototype!==Person)//true//实例通过__proto__链接到原型对象,它实际上指向隐藏的特征[[Prototype]]//构造函数通过原型属性链接到原型对象//实例不直接与构造函数相关,而是与原型对象相关Systemconsole.log(person1.__proto__===Person.prototype)//trueconsole.log(person1.__proto__.constructor===Person)//true//同一个构造函数创建的两个实例共享同一个Prototype对象console.log(person1.__proto__===person2.__proto__)//true//instanceof检查实例的原型链是否包含指定构造函数的原型console.log(person1instanceofPerson)//trueconsole.log(person1instanceofObject)//trueconsole.log(Person.prototypeinstanceofObject)//true上图展示了Person构造函数、Person的原型对象和已有的两个Person实例的关系注:Person.prototype指向prototype对象,而Person.prototype.constructor指回Person构造函数。原型对象包含constructor属性和后来添加的其他属性。Person的两个实例,person1和person2,都只有一个指向Person.prototype的内部属性,并且都没有直接连接到构造函数。还要注意,虽然两个实例没有属性和方法,但是可以正常调用person1.sayName(),这是由于对象属性查找机制的缘故。尽管并非所有的实现都将[[Prototype]]暴露给外界,但您可以使用isPrototypeOf()方法来确定两个对象之间的这种关系。本质上,isPrototypeOf()会在传入参数的[[Prototype]]指向调用它的对象时返回true,如下所示console.log(Person.prototype.isPrototypeOf(person1)//trueconsole.log(Person.prototype.isPrototypeOf(person2))//true这里调用isPrototypeOf()方法检查person1和person2。因为这两个例子都有指向Person.prototype的内部链接,所以结果都返回true。ESMAScript的Object类型有一个方法叫Object.getPrototypeOf(),返回参数[[Prototype]]的内部属性。例如:console.log(Object.getPrototypeOf(person1)===Peron.prototype//trueconsole.log(Object.getPrototypeOf(person1).name//'66'第一行代码简单确认Object.getPrototypeOf()返回的对象是传入对象的原型对象,第二行代码获取原型上name属性的值对象,它是'66'。使用Object.getPrototypeOf()can轻松获取对象的原型,这在通过原型实现继承时尤为重要。您可以使用Object.create()创建一个新对象并指定其原型:letbiped={numLegs:2}letperson=Object.create(biped)person.name='66'console.log(person.name)//'66'console.log(person.numLegs)//2console.log(Object.getPrototypeOf(person)===biped)//trueprototypelevel在通过对象访问属性时会跟随属性的名字开始搜索。搜索从对象实例本身开始。如果在此实例上找到给定名称,则返回与该名称对应的值。如果没有找到该属性,则查找会跟随指针进入原型对象,找到原型对象上的属性后返回相应的值。因此,当调用person1.sayName()时,会发生两次搜索。首先,JavaScript引擎会询问:“person1实例是否具有sayName属性?”答案是不。然后,继续搜索并询问:“person1的实例是否具有sayName属性?”答案是肯定的。所以返回保存在原型上的函数。调用person2.sayName()时相同。这就是原型如何用于在多个对象实例之间共享属性和方法。constructor属性只存在于原型对象上,因此也可以通过实例对象访问。虽然可以通过实例读取原型对象上的值,但不可能通过实例重写这些值。如果在实例中添加了一个与原型对象同名的属性,这个属性就会在实例上创建,这个属性会覆盖原型对象上的属性。让我们看一个例子:functionPerson(){}Person.prototype.name='66'Person.prototype.age=29Person.prototype.color='blue'Person.sayName=function(){console.log(this.name)}letperson1=newPerson()letperson2=newPerson()person1.name='51'console.log(person1.name)//'51'来自实例console.log(person2.name)//'66'来自原型在此示例中,person1的名称属性隐藏了原型对象上的同名属性。只要给对象实例添加一个属性,这个属性就会将原型对象上的同名属性隐藏起来,即虽然不会被修改,但会阻止对其的访问。即使在实例上将此属性设置为null也不会恢复其与原型的关联。但是,可以使用删除运算符完全删除实例上的属性,从而允许标识符解析过程继续搜索原型对象。hasOwnProperty()方法用于确定属性实际上是在实例上还是在原型对象上。此方法继承自Object,当调用它的对象实例上存在该属性时将返回true,如下例所示:console.log(person1.hasOwnProperty('name'))//trueconsole.log(person2.hasOwnProperty('name'))//false原型和in运算符in运算符有两种使用方式:单独使用和在for-in循环中使用。当单独使用时,如果指定的属性可通过对象访问,则in运算符返回true,无论该属性是在实例上还是在原型上。考虑以下示例:functionPerson(){}Person.prototype.name='66'Person.prototype.age=2Person.prototype.color='blue'Person.sayName=function(){console.log(this.name)}letperson1=newPerson()letperson2=newPerson()console.log(person1.hasOwnProperty('name'))//falseconsole.log('name'inperson2)//trueperson1.name='666'console.log(person1.name)//'666'来自实例console.log(person1.hasOwnProperty('name'))//tryeconsole.log('name'inperson1)//true控制台。log(person2.name)//“66”,来自原型console.log(person2.hasOwnProperty('name'))//falseconsole.log('name'inperson2)//truedeleteperson1.nameconsole.log(person1.name)//“66”,来自原型console.log(person1.hasOwnProperty('name'))//falseconsole.log('name'inperson1)//true在这个例子中,name总是可用的通过实例或原型访问。因此在person1中调用“name”总是返回true,无论该属性是否存在于实例中。如果你想确定一个属性是否存在于原型上,你可以像这样一起使用hasOwnProperty()和in运算符:只要可通过对象访问in运算符,它就会返回true,而hasOwnProperty()仅当该属性存在于实例上时才返回true。因此,只要in运算符返回true而hasOwnProperty()返回false,您就在原型上。当在for-in循环中使用in运算符时,返回所有可以通过对象访问和枚举的属性,包括实例属性和原型属性。原型中隐藏不可枚举([[Enumerable]]属性设置为false)属性的实例属性也会在for-in循环中返回,因为默认情况下开发人员定义的属性是可枚举的。要获取对象的所有可枚举实例属性,请使用Object.keys()方法。该方法以一个对象作为参数,并返回一个字符串数组,其中包含该对象的所有可枚举属性的名称。ESMAScript6中新的symbol类型之后,相应的需要增加一个Object.getOwnPropertyName()的兄弟方法,所以以symbol为key的property没有name的概念。于是,Object.getOwnPropertySymbols()方法出现了。此方法是Object.getOwnPropertyNames()类型,仅适用于符号。for-in循环的属性枚举顺序与Object.keys()、Object.getOwnPropertyName()、Object.getOwnPropertySymbols()、Object.assign()的属性枚举顺序有很大区别。for-in循环和Object.keys()的枚举顺序是不确定的,取决于JavaScript引擎,并且可能因浏览器而异。Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()和Object.assign()的枚举顺序是确定性的。数字键按升序枚举,然后是按插入顺序排列的字符串和符号键。对象字面量中定义的键以逗号分隔的顺序插入。
