前言:
在深入探索JavaScript的广阔领域时,原型(Prototype)和原型链(Prototype Chain)无疑是两个核心概念,它们不仅是理解JavaScript对象模型的关键,也是实现高效、可扩展代码的基础。作为JavaScript高级特性的重要组成部分,原型和原型链不仅揭示了JavaScript对象之间复杂而优雅的关系网,还为我们提供了一种强大而灵活的继承机制。
在JavaScript中,万物皆对象,而对象之间通过原型链相互连接,形成了一个错综复杂的网络。这个网络不仅允许对象之间共享方法和属性,还促进了代码的复用和模块化。掌握原型和原型链,意味着你能够更深入地理解JavaScript的底层机制,从而编写出更加高效、易于维护的代码。
本文将带你走进JavaScript原型和原型链的世界,从基础概念讲起,逐步深入其内部工作原理。我们将探讨如何利用原型来定义对象的行为,如何通过原型链实现继承,以及如何在实际项目中灵活运用这些高级特性。无论你是JavaScript的初学者还是有一定经验的开发者,相信本文都能为你提供宝贵的见解和实用的技巧。
原型(Prototype)
在JavaScript中,每个函数都有一个特殊的属性叫做prototype。这个属性是一个对象,当函数被用作构造函数来创建实例时,这个prototype对象将被用作新创建对象的原型。原型对象默认包含一个constructor属性,它指向与之关联的构造函数。
function f(){}
console.log(typeof f.prototype) //object
上述代码中可以看到函数f是具有prototype,通过typeof可以看到它的基本数据类型为object。
function Person(name) {
this.name = name;
}
Person.prototype.country = "China";
const userName1 = new Person('张三');
console.log(userName1.name); //张三
console.log(userName1.country); //China
const userName2 = new Person('李四');
console.log(userName2.name); //李四
console.log(userName2.country); //China
console.log(Person.prototype); //{country: 'China'}
在上述代码中我们首先定义了一个名为Person的构造函数,它接受一个参数name,并将该参数的值赋给新创建对象的name属性:
function Person(name) {
this.name = name;
}
接下来,我们向Person
函数的原型对象,也就是Person.prototype
添加了一个名为country
的属性,并将其值设置为"China":
Person.prototype.country = "China";
然后,我们创建了两个Person
的实例,分别命名为userName1
和userName2
,并为它们各自传递了不同的name
参数:
const userName1 = new Person('张三');
const userName2 = new Person('李四');
我们在分别去访问userName1
和userName2的name和country属性,并通过console.log()打印在浏览器上:
userName1的
name
属性的值为'张三',
这是通过构造函数直接设置的,而country
属性的值为'China',因此可以看出userName1是可以访问到Person函数的原型上,并继承该属性值。
userName2的
情况与userName1
相同,name
属性的值为'李四'
,而country
属性的值同样为'China',
这意味着所有通过new Person()
创建的实例都将继承这个country
属性。
最后,我们通过打印Person.prototype
对象来验证原型链上的属性。如我们所见,输出为{country: 'China'}
,这确认了country
属性确实被添加到了Person
的原型对象上。
其中new这个关键字做操作:
1.它会生成一个新的空对象。
2.会将构造函数中的this指向生成的新对象。
3.会执行函数中的代码。
4.返回这个新对象
其中原型对象上面的所有属性和方法都可以被实例对象共享。
原型(prototype)机制的一些优点和缺点:
优点:
1,内存效率:由于多个实例可以共享同一个原型上的方法和属性,这可以显著减少内存占用。相比于传统的基于类的继承,每个实例不需要自己单独存储一份方法副本。
2,灵活性:原型链允许在运行时动态地添加或修改方法和属性。这意味着你可以根据需要扩展对象的功能,而无需修改原始构造函数。
3,原型继承:原型链提供了一种自然的继承机制,允许对象从其他对象继承属性和方法。这使得实现复杂的继承层次结构成为可能。
缺点:
1,属性查找效率低:当访问一个对象的属性或方法时,JavaScript引擎需要沿着原型链逐级查找,直到找到为止或达到原型链的末端。这可能导致比直接访问对象自身属性更高的时间复杂度。
2,引用属性共享问题:如果原型上有一个引用类型的属性(如数组或对象),那么该属性将被所有实例共享。这可能会导致意外的行为,尤其是当某个实例修改了该属性时。
3,继承的复杂性:虽然原型链提供了一种继承机制,但它也增加了代码的复杂性。特别是当涉及多层继承时,需要仔细管理原型链,以避免意外的行为或性能问题。
4,缺乏封装性:与基于类的继承相比,原型链在封装方面较弱。因为所有实例都可以直接访问和修改原型上的属性和方法,这可能会破坏对象的封装性。
5,难以调试:由于原型链的复杂性和动态性,当出现问题时可能难以追踪和调试。特别是在大型应用程序中,原型链可能会变得非常长且难以管理。
尽管存在这些缺点,但原型机制仍然是JavaScript中一个非常重要的特性。通过深入理解其工作原理和优缺点,开发者可以更好地利用这一机制来构建高效、可扩展和可维护的JavaScript应用程序。
constructor 属性
在JavaScript中,每个函数都有一个特殊的属性叫做prototype
。这个prototype
对象是一个普通的对象,它包含一个constructor
属性,该属性指向函数本身。这个机制是JavaScript原型链(prototype chain)和继承系统的基础。
当创建一个构造函数(即一个用于创建新对象的函数)时,JavaScript会自动为这个构造函数的prototype
对象添加一个constructor
属性,这个属性指回构造函数本身。这允许对象通过原型链访问到它们的构造函数。
function Person(){};
var p1 = new Person();
console.log(Person.prototype.constructor === Person); //true
通过上述例子的打印结果可以看出,Person.prototype.constructor
指向了 Person
函数
console.log(p1.constructor === Person);
Person
的实例对象的constructor
属性指向了Person
constructor属性的作用是,可以得知某个实例对象,到底是哪一个构造函数产生的。
__proto__属性
在JavaScript中,__proto__
属性是一个非标准的、但广泛支持的方式来访问一个对象的原型(prototype)。然而,值得注意的是,尽管它在许多JavaScript环境中都可用,但__proto__
并不是一个官方属性,也不是ECMAScript标准的一部分。
尽管如此,__proto__
属性在实践中经常被用来查看或修改对象的原型链。它指向了创建该对象的构造函数的 prototype
属性。
function Person(){};
var p1 = new Person();
console.log(p1.__proto__ === Person.prototype); //true
上述代码中Person的实例对象p1的.__proto__属性指向了Person.prototype原型对象
原型也是一个对象,既然是对象就会有 __proto__ 属性,该属性指向原型对象的原型。
原型链
原型链是JavaScript中对象继承的一种机制。当一个对象需要访问某个属性或方法时,如果这个对象本身没有这个属性或方法,JavaScript引擎就会去它的原型对象中寻找。这个过程会一直持续进行,直到找到该属性或方法,或者达到原型链的顶端(通常是Object.prototype
,它的原型是null
,表示原型链的结束)
绘制函数和对象完整的原型链
1,Object 内置对象
var o = new Object()
console.log(o.__proto__ === Object.prototype); //true
console.log(Object === Object.prototype.constructor); //true
console.log(Object.prototype === Object.prototype); //true
console.log(o.constructor === Object); //true
console.log(Object.prototype.__proto__); //null
最后可以看出Object.prototype原型对象它的原型指向了null,表示原型链的结束。
2,Person 自定义对象
function Person(){}
var p1 = new Person();
console.log(p1.__proto__ === Person.prototype); //true
console.log(Person.prototype.constructor === Person); //true
console.log(Person.prototype === Person.prototype); //true
console.log(p1.constructor === Person); //true
3,Function 特殊对象
var f = new Function()
在JavaScript中,Function
是一个特殊的内置对象,它用于创建新的函数。
console.dir(Function.prototype) //ƒ anonymous()
console.dir(Function.__proto__) //ƒ anonymous()
console.log(Function.prototype === Function.__proto__); //true
上述代码可以看出Function特殊对象,Function对象它可以通过prototype属性指向它的它的原型,也可以通过__proto__属性指向原型,并且他们的返回值全等,这看起来可能有些反直觉,但这是因为 Function
本身也是一个函数对象(是的,函数也是对象)。因此,Function
函数对象也有一个 [[Prototype]]
指向 Function.prototype
。
console.log(Function.prototype.constructor === Function); //true
这是正确的。在JavaScript中,每个原型对象都有一个constructor
属性,它指向用于创建该原型对象的构造函数。因此,Function.prototype.constructor
确实指向Function
构造函数。
以上三个对象的小原型链我们已经化完了,但我们需要把这三个都连在一起。
4,完整的原型链
console.log(Person.prototype.__proto__ === Object.prototype); //true
console.log(Function.prototype.__proto__ === Object.prototype); //true
上述代码可以看出Person.prototype原型对象它的__proto__以及Function.prototype原型对象它的__proto__都指向了Object.prototype这个原型对象。因此可以绘制出:
console.log(Object.__proto__ === Function.prototype); //true
console.log(Person.__proto__ === Function.prototype); //true
上述代码可以看出Object内置对象的__proto__属性以及Person内置对象的__proto__属性都指向了Function.prototype原型对象。因此可以绘制出:
其中Function这个对象比较特殊:
1,所有函数都是Function的实例:在JavaScript中,当你定义一个函数时,无论是通过函数声明、函数表达式、箭头函数,还是使用 new Function()
构造函数,创建的这个函数对象都是 Function
类型的实例。这意味着你可以在这些函数对象上访问到 Function.prototype
上定义的任何属性和方法。
2,Function自己是自己的实例:这个特性听起来可能有点反直觉,但它是JavaScript语言设计的一部分。由于 Function
是一个构造函数,它本身也是一个函数。在JavaScript中,函数是一等公民,这意味着函数可以像其他任何值一样被传递、赋值给变量或作为参数。因此,Function
构造函数(作为一个函数)也是由某个构造函数创建的,而这个构造函数恰好就是它自己。
我们就可以绘制出一个基本上完整的原型链了: