09-原型
9.2.4 原型
在类的实例中,当引用一个方法时,实际上是在引用原型方法。例如,当讨论 Car 的实例中的 shift 方法时,引用的是一个原型方法,并且会看到它被写成 Car.prototype.shift 。(类似的,数组的 forEach 函数会被写成 Array.prototype.forEach. )现在是时候好好学习什么是原型了,以及JavaScirpt中如何使用原型链进行动态调度的。
使用数字符号(#)来描述原型方法已经成为一种普遍的约定。例如,大家会经常看到 `Car.prototype.shift` 被简单地写成 `Car#shift` 。
每个函数都有一个叫作 prototype 的特殊属性(可以通过在控制台中输入 <strong>f.prototype</strong> 来将它更改为任意函数)。一般的函数不需要使用原型,但是对于那些作为对象构造器的函数,它就至关重要了。
按照约定,对象的构造器(又指类名)始终以大写字母开头,例如 `Car` 。这个约定并非强制的,但如果试图以大写字母开头来命名某个函数,或者以小写字母给构造器命名时,很多格式检查工具都会发出警告。
当使用关键字 new 来创建一个新的实例时,函数的原型属性就会变得很重要:新创建的对象可以访问其构造器的原型对象。对象实例会将它存储在自己的 __proto__ 属性中。
`__proto__` 属性是JavaScript的内部属性,就像任何被双下划线包围的属性一样。可以用这些属性做一些非常邪恶的事情。有时候它们也有一些聪明并有效的使用场景,但只有在完全理解了JavaScript之后才知道如何使用它。强烈建议大家看看这些属性但是不要改变它们。
关于原型,有一个重要的机制叫作动态调度(“调度”是方法调用的另一种说法)。当试图访问对象的某个属性或者方法时,如果它不存在于当前对象中,JavaScritp会检查它是否存在于对象原型中。因为同一个类的所有实例共用同一个原型,如果原型中存在某个属性或者方法,则该类的所有实例都可以访问这个属性或方法。
通常不要在类的原型中设置数据属性,因为所有的实例都会共享这些属性的值。但是如果该值被设置在任意一个实例中,那么它就只在特定的实例中存在,而原型中不存在,这会引发困惑和产生bug。所以如果需要给对象实例初始化一些数据,最好是通过构造器来设置。
注意,在实例中定义的方法或者属性会覆盖掉原型中的定义。记住JavaScript的检查顺序是先实例后原型。下面来看看它们的实际用法:
// 类Car 跟之前定义的一样, 具备移动方法
const car1 = new Car();
const car2 = new Car();
car1.shift === Car.prototype.shift; // true
car1.shift('D');
car1.shift('d'); // error
car1.userGear; // 'D'
car1.shift === car2.shift // true
car1.shift = function(gear) { this.userGear = gear.toUpperCase(); }
car1.shift === Car.prototype.shift; // false
car1.shift === car2.shift; // false
car1.shift('d');
car1.userGear; // 'D'
这个例子很清楚地演示了JavaScirpt执行动态调度的方式。一开始,对象 car1 没有 shift 方法,但是当调用 car1.shift('D') 时,JavaScript就会查看car1的原型,找到一个同名的方法。当用自定义的方法替换掉shift后,car1自身和它的原型都有 shift 方法了。再调用 car1.shift('d') 时,实际上调用了 car1 自己的 shift 方法。
大多数时候,不必搞懂原型链和动态调度的机制。但在不同的阶段,可能会遇到一些迫使大家去深入学习它们的问题。最好在学习的时候能都明白其中的细节。
使用数字符号(#)来描述原型方法已经成为一种普遍的约定。例如,大家会经常看到 `Car.prototype.shift` 被简单地写成 `Car#shift` 。
`__proto__` 属性是JavaScript的内部属性,就像任何被双下划线包围的属性一样。可以用这些属性做一些非常邪恶的事情。有时候它们也有一些聪明并有效的使用场景,但只有在完全理解了JavaScript之后才知道如何使用它。强烈建议大家看看这些属性但是不要改变它们。
通常不要在类的原型中设置数据属性,因为所有的实例都会共享这些属性的值。但是如果该值被设置在任意一个实例中,那么它就只在特定的实例中存在,而原型中不存在,这会引发困惑和产生bug。所以如果需要给对象实例初始化一些数据,最好是通过构造器来设置。