本文深入解析JavaScript中prototype属性的核心机制,涵盖构造函数与原型链的关系、constructor属性的作用及修改时的注意事项,以及instanceof运算符的判断逻辑。通过具体代码示例,阐明对象属性共享、原型链查找规则及常见陷阱,帮助开发者掌握ES6 Class之前的面向对象编程基础。
在Java等典型面向对象语言中,类(class)作为对象的模板存在,对象则是类的实例。然而,JavaScript语言体系中并未直接引入“类”的概念,而是通过构造函数(constructor)和原型链(prototype chains)来实现面向对象特性。尽管ES6引入了更接近传统语言的Class关键字,但这主要被视为一种语法糖,其底层功能在ES5中已可通过原型机制实现。新的Class写法旨在使原型对象的定义更加清晰,符合传统面向对象编程的语法习惯。
按照写作习惯,在展开具体内容前,先列出文章目录以便阅读。
以下内容将分为若干小节进行详细阐述:
1.构造函数的简单介绍
2.构造函数的缺点
3.prototype属性的作用
4.原型链(prototype chain)
5.constructor属性
5.1:constructor属性的作用
6.instanceof运算符
构造函数的基本概念与特点
1.构造函数的简单介绍
关于构造函数的概念、特点以及new命令的原理和用法,此前已有文章详细探讨,不熟悉的同学可参考相关教程。此处仅做简要回顾。
构造函数本质上是一个用于生成对象模板并描述对象基本结构的函数。一个构造函数可以创建多个对象,这些对象拥有相同的结构。简而言之,构造函数是对象的模板,而对象则是构造函数的实例。
构造函数具有以下特点:
a:构造函数的函数名首字母必须大写,以区别于普通函数。
b:在函数内部使用this关键字,指向即将生成的对象实例。
c:必须使用new操作符来调用构造函数,并返回对象实例。
以下是一个最简单的示例代码。
function Person(){
this.name = 'keith';
}
var boy = new Person();
console.log(boy.name); //'keith'
构造函数的局限性
2.构造函数的缺点
虽然所有实例对象都能继承构造函数中的属性和方法,但同一构造函数的不同对象实例之间无法共享属性。
function Person(name,height){
this.name=name;
this.height=height;
this.hobby=function(){
return 'watching movies';
}
}
var boy=new Person('keith',180);
var girl=new Person('rascal',153);
console.log(boy.name); //'keith'
console.log(girl.name); //'rascal'
console.log(boy.hobby===girl.hobby); //false
在上述代码中,构造函数Person生成了boy和girl两个对象实例,它们各自拥有属性和一个hobby方法。然而,这两个实例的hobby方法并不相同。这意味着每次使用new调用构造函数创建对象实例时,都会重新创建一个hobby方法。这种做法不仅没有必要,还浪费资源,因为所有hobby方法执行的行为完全相同,完全可以被两个对象实例共享。
因此,构造函数的主要缺点在于:同一构造函数的对象实例之间无法共享属性或方法。
prototype属性的核心作用
3.prototype属性的作用
为了解决构造函数对象实例之间无法共享属性的问题,JavaScript提供了prototype属性。
在JavaScript中,除了null和undefined外,所有数据类型都是对象。每个对象都继承自另一个对象,后者称为“原型”(prototype)对象,null除外,它没有自己的原型对象。
原型对象上的所有属性和方法,都会被其对象实例所共享。
通过构造函数生成对象实例时,对象实例的原型会被指向构造函数的prototype属性。每个构造函数都有一个prototype属性,该属性即为对象实例的原型对象。
function Person(name,height){
this.name=name;
this.height=height;
}
Person.prototype.hobby=function(){
return 'watching movies';
}
var boy=new Person('keith',180);
var girl=new Person('rascal',153);
console.log(boy.name); //'keith'
console.log(girl.name); //'rascal'
console.log(boy.hobby===girl.hobby); //true
在上述代码中,若将hobby方法放置在原型对象上,则两个实例对象将共享同一个方法。需要理解的是,对于构造函数而言,prototype是一个属性;而对于对象实例而言,prototype是其原型对象。因此,prototype既是属性,也是对象。
原型对象的属性并非对象实例自身的属性。对象实例的属性继承自构造函数中通过this关键字定义的属性。只要修改原型对象上的属性和方法,所有对象实例都会立即反映这些变动。
Person.prototype.hobby=function(){
return 'swimming';
}
console.log(boy.hobby===girl.hobby); //true
console.log(boy.hobby()); //'swimming'
console.log(girl.hobby()); //'swimming'
在上述代码中,当修改原型对象的hobby方法后,两个对象实例均发生变化。这是因为对象实例自身并没有hobby方法,而是读取原型对象上的hobby方法。也就是说,当某个对象实例缺少某个属性或方法时,会去原型对象中查找。如果实例对象自身拥有该属性或方法,则不会去原型对象上查找。
boy.hobby=function(){
return 'play basketball';
}
console.log(boy.hobby()); //'play basketball'
console.log(girl.hobby()); //'swimming'
在上述代码中,当boy对象实例修改其hobby方法时,它将不再继承原型对象上的hobby方法。不过,girl实例仍会继承原型对象的方法。
总结如下:
a:原型对象的作用在于定义所有对象实例所共享的属性和方法。
b:prototype对于构造函数来说是一个属性;对于对象实例来说,它是一个原型对象。
原型链的查找机制
4.原型链(prototype chains)
对象的属性和方法可能定义在对象自身,也可能定义在其原型对象中。由于原型对象本身也是对象,它也有自己的原型,从而形成了一条原型链(prototype chain)。例如,a对象是b对象的原型,b对象是c对象的原型,依此类推。所有一切对象的原型顶端都是Object.prototype,即Object构造函数的prototype属性所指向的对象。
当然,Object.prototype对象也有自己的原型对象,那就是没有任何属性和方法的null对象,而null对象没有自己的原型。
1 console.log(Object.getPrototypeOf(Object.prototype)); //null
2 console.log(Person.prototype.isPrototypeOf(boy)) //true
原型链(prototype chain)具有以下特点:
a:读取对象的某个属性时,JavaScript引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype还是找不到,则返回undefined。
b:如果对象自身和它的原型都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overiding)。
c:一级级向上在原型链寻找某个属性,对性能是有影响的。所寻找的属性在越上层的原型对象,对性能的影响越大。如果寻找某个不存在的属性,将会遍历整个原型链。
概念可能较为晦涩,我们通过一个例子来理解。尽管理解概念很重要,但通过实例分析更为直观。
var arr=[1,2,3];
console.log(arr.length); //3
console.log(arr.valueOf()) //[1,2,3]
console.log(arr.join('|')) //1|2|3
在上述代码中,定义了一个数组arr,包含三个元素。我们并未给数组添加任何属性和方法,但在调用length、join()、valueOf()时却不会报错。
length属性继承自Array.prototype,属于原型对象上的一个属性。join方法也继承自Array.prototype,属于原型对象上的一个方法。这两个方法是所有数组所共享的。当实例对象上没有该length属性时,就会去原型对象查找。
valueOf方法继承自Object.prototype。首先,arr数组没有valueOf方法,因此到原型对象Array.prototype查找。然后,发现Array.prototype对象上没有valueOf方法。最后,再到其原型对象Object.prototype查找。
接下来,我们来看看Array.prototype对象和Object.prototype对象分别有哪些属性和方法。
console.log(Object.getOwnPropertyNames(Array.prototype))
//["length", "toSource", "toString", "toLocaleString", "join", "reverse", "sort", "push", "pop", "shift", "unshift", "splice", "concat", "slice", "lastIndexOf", "indexOf", "forEach", "map", "filter", "reduce", "reduceRight", "some", "every", "find", "findIndex", "copyWithin", "fill", "entries", "keys", "values", "includes", "constructor", "$set", "$remove"]
console.log(Object.getOwnPropertyNames(Object.prototype))
// ["toSource", "toString", "toLocaleString", "valueOf", "watch", "unwatch", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "__defineGetter__", "__defineSetter__", "__lookupGetter__", "__lookupSetter__", "__proto__", "constructor"]
相信读者阅读至此,对prototype仍可能似懂非懂。这很正常,毕竟这是JavaScript中重要且抽象的概念,不可能快速完全掌握。建议多阅读几篇相关文章,或许能掌握其精髓。在某乎上,有一个生动的实例,可能也是大家会遇到的问题,可以参考“js构造函数和原型对象”一文。
constructor属性的定义与用途
5.constructor属性
prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。
function A(){};
console.log(A.prototype.constructor===A) //true
需要注意的是,prototype是构造函数的属性,而constructor则是构造函数的prototype属性所指向的那个对象,也就是原型对象的属性。注意不要混淆。
console.log(A.hasOwnProperty('prototype')); //true
console.log(A.prototype.hasOwnProperty('constructor')); //true
由于constructor属性定义在原型(prototype)对象上,意味着它可以被所有实例对象继承。
function A(){};
var a=new A();
console.log(a.constructor); //A()
console.log(a.constructor===A.prototype.constructor);//true
在上述代码中,a是构造函数A的实例对象,但a自身没有constructor属性,该属性实际上是读取原型链上的
A.prototype.constructor属性。
5.1:constructor属性的作用
a:分辨原型对象到底属于哪个构造函数
function A(){};
var a=new A();
console.log(a.constructor===A) //true
console.log(a.constructor===Array) //false
上述代码表示,使用constructor属性,可以确定实例对象a的构造函数是A,而不是Array。
b:从实例新建另一个实例
function A() {};
var a = new A();
var b = new a.constructor();
console.log(b instanceof A); //true
在上述代码中,a是构造函数A的实例对象,可以从a.constructor间接调用构造函数。
c:调用自身的构造函数成为可能
A.prototype.hello = function() {
return new this.constructor();
}
d:提供了一种从构造函数继承另外一种构造函数的模式
function Father() {}
function Son() {
Son.height.constructor.call(this);
}
Son.height = new Father();
在上述代码中,Father和Son都是构造函数,在Son内部的this上调用Father,就会形成Son继承Father的效果。
e:由于constructor属性是一种原型对象和构造函数的关系,所以在修改原型对象的时候,一定要注意constructor的指向问题。
解决方法有两种,要么将constructor属性指向原来的构造函数,要么只在原型对象上添加属性和方法,避免instanceof失真。
instanceof运算符的应用
6.instanceof运算符
instanceof运算符返回一个布尔值,表示指定对象是否为某个构造函数的实例。
function A() {};
var a = new A();
console.log(a instanceof A); //true
因为instanceof对整个原型链上的对象都有效,所以同一个实例对象,可能会对多个构造函数都返回true。
function A() {};
var a = new A();
console.log(a instanceof A); //true
console.log(a instanceof Object); //true
注意,instanceof运算符只能用于复杂数据类型(数组、对象等),不能用于简单数据类型(布尔值、数字、字符串等)。
var x = [1];
var o = {};
var b = true;
var c = 'string';
console.log(x instanceof Array); //true
console.log(o instanceof Object); //true
console.log(b instanceof Boolean); //false
console.log(c instanceof String); //false
此外,null和undefined都不是对象,所以instanceof 总是返回false。
console.log(null instanceof Object); //false console.log(undefined instanceof Object); //false
利用instanceof运算符,还可以巧妙地解决调用构造函数时忘记加new命令的问题。
function Keith(name,height) {
if (! this instanceof Keith) {
return new Keith(name,height);
}
this.name = name;
this.height = height;
}
在上述代码中,使用了instanceof运算符来判断函数体内的this关键字是否指向构造函数Keith的实例,如果不是,就表明忘记加new命令,此时构造函数会返回一个对象实例,避免出现意想不到的结果。
限于篇幅原因,暂时介绍到这里。
我会在下次的分享中谈谈原型(prototype)对象的一些原生方法,如Object.getPrototypeOf()、Object.setPrototypeOf()等,并且介绍获取原生对象方法的比较。
以上所述是小编给大家介绍的详解Javascript中prototype属性(推荐)的相关知识,希望对大家有所帮助。