彻底弄懂JavaScript原型和原型链问题

yancy

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实例化的dogcat类,都没有声明sayName函数,但是它们都有这个函数可以执行。而实例对象一旦创建,将自动引用prototype对象的属性和方法。

相信你已经get到了这个知识点,别急,继续看下面这种情况。

// 再次定义Animal。
function Animal() {}
Animal.prototype.eat = function () {
  console.log('吃东西')
}

let dog = new Animal();
dog.eat = function () {
  console.log('我只吃骨头!')
}

dog.eat(); // 我只吃骨头

注意:

上述例子说明:只有在dogcat上找不到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

这是因为dogAnimal的一个实例,所以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的关系

关系太复杂,只能通过图解来说明了。

<center><img src="./1.jpeg"></center>

3.原型链

知道了三者的概念,了解了它们的用途,接下来想弄懂原型链就会变得简单了。原型链就是无数个上图的串联。我们还是通过图解的方式来看。

<center><img src="./5.jpeg"></center>

图太长了,不过千万别被图吓到哟。原型链就是从实例对象开始,通过 __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)
阅读 143

你若盛开,清风自来

12 声望
1 粉丝
0 条评论

你若盛开,清风自来

12 声望
1 粉丝
宣传栏