彻底弄懂JavaScript原型问题
JavaScript
中的原型和原型链问题,一直是困扰新手乃至于广大前端工程师的重要问题,的确,相对于普通语法来说,它会更加难以理解,在日常开发过程中也不常见。但是……它的重要性是不言而喻的。下面我们就来探究一下。
进入主题
1.三个重要属性
要理解原型问题,先要了解以下三个属性。
- prototype
- __protp__
- constructor
进入逐一讲解阶段:
1.1 prototype
JavaScript
中的每个构造函数都有prototype
对象。所有实例对象需要共享的属性和方法都放在这个对象里。而那些不需要共享的,就放在构造函数中。使用过程中,不需要我们手动声明一个prototype
属性。有了它,我们就可以将所有需要共享的方法提取到一处,避免冗余。通过一个小栗子来看下
// 定义一个动物的构造函数。
function Animal(name) {
// 构造函数中存放的是不需要共享的方法和属性
this.name = name; // 定义每个动物的名字
}
// 将每个动物需要共享的方法和属性放到一块
Animal.prototype.eat = function () {
console.log('吃东西')
}
// 输出自己的名字
Animal.prototype.sayName = function () {
console.log(this.name)
}
let dog = new Animal('狗'); // 实例化一个狗的对象
let cat = new Animal('猫'); // 实例化一个猫的对象
dog.sayName(); // 狗
cat.sayName(); // 猫
dog.eat();
cat.eat();
这里,我们通过Animal
实例化的dog
和cat
类,都没有声明sayName
函数,但是它们都有这个函数可以执行。而实例对象一旦创建,将自动引用prototype对象的属性和方法。
相信你已经get到了这个知识点,别急,继续看下面这种情况。
// 再次定义Animal。
function Animal() {}
Animal.prototype.eat = function () {
console.log('吃东西')
}
let dog = new Animal();
dog.eat = function () {
console.log('我只吃骨头!')
}
dog.eat(); // 我只吃骨头
注意:
上述例子说明:只有在dog
和cat
上找不到eat
方法的时候才会向Animal
查找,如果能查找到,则不会执行Animal
的方法。这种情况也叫做方法的重写。
上面的例子相信你已经看的很明白了,不明白也问题不大,下面的讲解我们还会通过实际的问题来介绍。接下来看第二个重要的属性。
看的累了就休息会儿吧,下面的内容更精彩…………
1.2 __proto__
很多人容易将 prototype
与 __proto__
(双下划线) 混淆,因为它们之间的指向有点儿绕。不过,一通则百通😝,等你真正理解了,就会发现,其实挺简单的。
在很多情况下,__proto__
可以理解为 构造器的原型,这是为什么呢?接着看小栗子吧。
function Animal () {}
let dog = new Animal();
// 重点来了…………
console.log(dog.__proto__ === Animal.prototype) // true
可以看到,dog.__proto__
和Animal.prototype
是完全相等的。好了,基本使用看完了,我们来探究下__proto__
的指向问题吧。
1.2.1 __proto__的指向问题
可能大家不太理解,为什么__proto__
还有指向。这里需要做个说明,通过不同方式创建出来的对象,它的__proto__
指向是不同的。
1.字面量方式创建对象
这个大家比较容易理解,日常工作中我们也是使用的比较多。
let dog = {}
console.log(dog.__proto__) // Object {} --> Object的原型
// 验证是否相等
console.log(dog.__proto__ === Object.prototype) // true
这里因为直接给dog
赋值为了全局的对象,所以__proto__
指向了Object.prototype
😎。
2.构造器方式创建
话不多说,直接上栗子。
function Animal() {}
let dog = new Animal()
console.log(dog.__proto__) // A {} --> A的原型
// 验证
console.log(dog.__proto__ === Animal.prototype) // true
这是因为dog
是Animal
的一个实例,所以dog
的__proto__
指向了Animal.prototype
。
3.Object.create 方式创建
let Animal = {
c: 1 // 为了区分,我们这里加一个自定义属性。
}
let dog = Object.create(Animal)
console.log(dog.__proto__) // Animal {c: 1}
//验证
console.log(dog.__proto__ === Animal) // true
注意:
Object.create()
方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
。也就是说,这里直接将Animal
作为了dog
的__proto__
。
1.3 constructor
这个属性不必多说,指的就是构造函数,每个对象都会有一个constructor
属性来指向自己的构造函数
function Animal() {} // 继续使用这个使用了很多次的Animal构造函数
console.log(Animal === Animal.prototype.constructor) // true
这里的Animal.prototype.constructor
就指向了 Animal
函数。
重点来了👇👇👇👇👇👇👇
2.__proto__、prototype和constructor的关系
关系太复杂,只能通过图解来说明了。
3.原型链
知道了三者的概念,了解了它们的用途,接下来想弄懂原型链就会变得简单了。原型链就是无数个上图的串联。我们还是通过图解的方式来看。
图太长了,不过千万别被图吓到哟。原型链就是从实例对象开始,通过 __proto__
来向上查找,直到找到null
为止。
附加项:
看到这里,大家可能会有一个疑问。每个构造函数都有 prototype
对象, 那么这个对象到底指向到了哪儿?
其实,每个prototype
对象的最终指向都是Object.prototype
。为什么这么说,是因为每个构造函数在创建的时候会分配一块儿空间来存储需要共享的方法和属性,所以 prototype
是通过对象创建来的,那么它的最终指向肯定也是Object.prototype
。老规矩,我们来通过栗子来验证一下。
function dog(){}
function cat(){}
function monkey(){}
console.log(dog.prototype.__proto__ === Object.prototype) // true
console.log(cat.prototype.__proto__ === Object.prototype) // true
console.log(monkey.prototype.__proto__ === Object.prototype) // true
4.面试题理解
4.1 请查看下列程序,会输出什么
function F() {}
Object.prototype.a = function () {}
Function.prototype.b = function () {}
var f = new F()
F.a()
F.b()
f.a()
f.b()
解析:
F.a F.b f.a
执行没问题,因为即使在构造函数总找不到这两个函数,但是通过原型链查找到。
-
F.a
函数的查找顺序是F构造函数本身 --> Function.prototype --> Function.prototype.__proto__
-
F.b
函数的查找顺序是F构造函数本身 --> Function.prototype
-
f.a
函数的查找顺序是f本身 --> f._proto__(Function.prototype) --> f.__proto__._proto__(Object.prototype)
而 f.b
则会出现执行出错的情况
原因是这样的:
查找顺序和f.a
相同,但是,在f.__proto__._proto__
,也就是Object.prototype
上查找不到 b
函数,最终导致查找失败,出现错误。
聪明的你肯定学会了,接下来挑战一下吧!!!
function User() {}
User.prototype.hello = function () {}
let u1 = new User()
let u2 = new User()
console.log(u1.hello === u2.hello)
console.log(User.prototype.constructor)
console.log(User.prototype === Function.prototype)
console.log(User.__proto__ === Function.prototype)
console.log(User.prototype === Function.__proto__)
console.log(u1.__proto__ === u2.__proto__)
console.log(u1.__proto__ === User.__proto__)
console.log(Function.__proto__ === Object.__proto__)
console.log(Function.prototype.__proto__ === Object.prototype.__proto__)
console.log(Function.prototype.__proto__ === Object.prototype)