JS-原型链与作用域

55 阅读3分钟

前言

在 JavaScript 面试中,有两座大山:一座是作用域链(管变量查找),另一座就是我们今天要讲的原型链(管属性查找和继承))。很多同学容易搞混这三者:prototype__proto__constructor。今天我们就用最通俗的语言把它们理清楚

一、 核心概念

要理解原型链,必须先搞懂这三个属性的关系。

1. prototype (显式原型)

  • 归属只有函数(构造函数) 才有这个属性。
  • 作用:它是构造函数的一个属性,通过这个属性可以指向一个对象,这个对象就是该构造函数的原型

2. __proto__ (隐式原型)

  • 归属所有对象(包括函数,因为函数也是对象)都有这个属性。
  • 作用:它是js对象的一个属性,通过这个属性可以指向该对象的原型(对象原型和构造函数原型是同一个)。

3. constructor (构造器)

  • 归属原型对象 拥有的属性。
  • 作用:它指回关联的那个构造函数本身(“生父”)。
  • 关系Person.prototype.constructor === Person


二、 原型链的工作原理

什么是原型链? 当我们访问一个对象实例的属性或方法时,查找规则如下:

  1. 首先查找对象本身是否有该属性。
  2. 如果没有,浏览器会沿着对象的 __proto__ 属性,去它的原型对象中查找。
  3. 如果原型对象中还没有,就继续沿着原型的 __proto__ 向上查找。
  4. 这个过程一直持续到链条的顶端 —— Object.prototype
  5. 如果还找不到,Object.prototype.__proto__null,此时返回 undefined

这条由 __proto__ 串联起来的链条,就是原型链

代码实战

function Person(name) {
  this.name = name;
}

// 在原型上添加方法
Person.prototype.sayHello = function() {
  console.log('Hello');
}

const p1 = new Person('Jack');

console.log(p1.name);      // 'Jack' (自身属性)
p1.sayHello();             // 'Hello' (自身没有,去 p1.__proto__ 也就是 Person.prototype 找)
console.log(p1.toString());// '[object Object]' (Person.prototype 也没有,去 Object.prototype 找)

三、 图解:链条的尽头在哪里?

这是一个非常经典的面试题:原型链有终点吗?

答案是:有,终点是 null

  • p1.__proto__Person.prototype
  • Person.prototype.__proto__Object.prototype
  • Object.prototype.__proto__null (这就是尽头)

四、 面试模拟题(挑战一下)

Q1:说一下作用域链和原型链的区别?

参考回答: 这是一个概念纠错题。

  • 作用域链:是针对变量访问的。在当前执行上下文找不到变量时,会去外层词法作用域查找,直到全局作用域。
  • 原型链:是针对对象属性访问的。读取属性时,如果对象自身没有,会沿着 __proto__ 链去原型对象查找,直到 null

Q2:FunctionObject 是什么关系?(高难度)

参考回答: 在 JS 中,所有函数都是 Function 的实例,所有对象都是 Object 的实例。

  • Object 本身是个构造函数,所以它是 Function 的实例:Object.__proto__ === Function.prototype
  • Function 本身也是个对象,所以它继承自 ObjectFunction.prototype.__proto__ === Object.prototype。 这就是著名的“鸡生蛋,蛋生鸡”闭环。

Q3:如何判断一个属性是对象自身的,还是继承自原型的?

参考回答: 使用 hasOwnProperty 方法。

const obj = { a: 1 };
// b 是继承自原型的
obj.hasOwnProperty('a'); // true
obj.hasOwnProperty('toString'); // false