Published on

JavaScript 原型与原型链详解

Authors
  • avatar
    Name
    青雲
    Twitter

JavaScript是一种基于原型的语言,这意味着对象间的继承是通过原型(prototype)实现的。要理解JavaScript的工作方式,掌握原型和原型链的概念是至关重要的。

原型(prototype)

JavaScript的原型是一个内部属性,所有通过函数创建的对象都会有一个隐藏的属性[[Prototype]],在ECMAScript 5+的代码中通常通过__proto__属性来访问。原型的主要用途是实现对象的继承:当试图访问一个对象的属性时,如果对象本身没有这个属性,那么JavaScript就会去它的原型中查找。

构造函数是普通的函数,用new操作符调用时,它们将创建新的对象。在函数内部,this关键字指向新创建的对象。所有函数(除了箭头函数)都有一个名为prototype的属性,它是一个对象。当你用构造函数创建一个新对象时,这个新对象的内部[[Prototype]]属性(即__proto__)被赋值为构造函数的prototype对象。

__proto__属性(在ECMAScript 2015以前是非标准的,但许多环境支持)是对象的内部属性,它是对对象原型的引用。准确来讲,__proto__[[Prototype]]内部属性的gettersetter,而[[Prototype]]是真正的原型链接。

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

// 将方法放在原型上,所有使用Person创建的实例都可以访问
Person.prototype.sayHello = function() {
  console.log('Hello, my name is ' + this.name);
};

const alice = new Person('Alice');
alice.sayHello(); // 访问原型链上的方法
console.log(alice.__proto__ === Person.prototype); // 输出 true
console.log(alice.__proto__); 

image.png

Prototype__proto__的联系

  1. ConstructorFunction 表示任意的构造函数,有一个 prototype 属性指向 PrototypeObject
  2. PrototypeObject 表示构造函数 prototype 属性指向的对象,有一个 constructor 属性回指 ConstructorFunction
  3. InstanceObject 表示通过 new ConstructorFunction() 创建的实例,它有一个 __proto__ 属性链接到 PrototypeObject

原型链

JavaScript的原型链是一种基于原型的继承机制,用于解析和查找对象属性和方法。在JavaScript中,几乎所有对象都是Object的实例,继承自Object的方法和属性。例如,数组对象继承自Array.prototype,数组方法如.forEach.map等都定义在这里,但所有数组实例也能访问Object.prototype上的方法,比如.toString.hasOwnProperty

原型链的工作原理

当你尝试访问一个对象的属性时,JavaScript首先检查这个对象本身是否有这个属性。如果没有,它会查找这个对象的原型(也就是__proto__属性或者通过Object.getPrototypeOf()得到的对象),继续在这个原型上查找属性。如果仍然找不到,它会查找原型的原型,以此类推,直到找到该属性或者到达原型链的顶端——通常是Object.prototype。 如果连Object.prototype上也找不到所需的属性,那么结果就是undefined,这说明该属性在原型链上不存在。这条搜索路径就形成了一条“链”,即我们所说的原型链。 所有对象的原型链最终都会指向Object.prototype(JavaScript中几乎所有对象的基础原型),除非明确设置其原型为nullObject.prototype的原型是null,表示没有更上一层的原型,原型链在此终结。

原型链的作用

原型链主要实现了两个方面的功能:

  1. 属性查找:当访问对象的属性时,如果对象本身不存在这个属性,JavaScript会沿着原型链向上查找,直到找到属性或到达原型链的末端。
  2. 实现继承:通过原型链,一个对象可以使用另一个对象的属性和方法,实现继承。创建新对象时,可以指定这个新对象的原型为另一个对象的实例,从而继承该对象的属性和方法。

示例

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

Animal.prototype.speak = function() {
  console.log(`${this.name} makes a noise.`);
};

function Dog(name) {
  Animal.call(this, name);  // 调用父类构造函数
}

Dog.prototype = Object.create(Animal.prototype);  // 继承
Dog.prototype.constructor = Dog;

Dog.prototype.speak = function() {
  console.log(`${this.name} barks.`);
};

const dog = new Dog('Rex');
dog.speak();  // 输出: Rex barks

在这个示例中,Dog 类继承了 Animal 类。Dog.prototype 的原型是 Animal.prototype,因此 dog 实例可以访问 Animalspeak 方法。

console.log(dog.__proto__);  // Dog.prototype
console.log(dog.__proto__.__proto__);  // Animal.prototype
console.log(dog.__proto__.__proto__.__proto__);  // Object.prototype
console.log(dog.__proto__.__proto__.__proto__.__proto__);  // null

image.png

原型链在现代JS框架中的应用

在现代 JavaScript 框架中,原型链的应用通常被抽象化,并且不像在原生 JavaScript 编程中那样明显。React 和 Vue 等框架隐藏了大多数直接操作原型链的需求,但底层仍然依赖于 JavaScript 的原型继承机制。

React

React 是一个使用 JavaScript 构建用户界面的库,早期版本中通过 React.createClass 方法创建类,并且有显式的原型链继承。然而,自从 ES6 引入了类语法后,React 推荐使用 ES6 类来创建组件。 在 ES6 类组件中,React 使用原型链来继承 React.Component 的方法,如 render, setState, forceUpdate 等,这是内部使用原型链的一个例子。 然而,React 还大量采用了组合而不是继承来代码重用,如高阶组件(HOC)、Render Props和Hooks。

class MyComponent extends React.Component {
  // 组件方法可以使用继承自 React.Component 的方法
}

Vue

Vue 是一个渐进式 JavaScript 框架,专注于构建用户界面。Vue 的核心概念包括响应式系统、组件化等,这些概念在底层也使用了原型链。 组件实例在内部通过 Vue 构造器创建,其中部分原型链继承是由 Vue 构造器的 extend 方法实现的。Vue 组件的生命周期钩子、数据观察和事件系统等都是通过这种方式挂载到 Vue 实例上的。

import Vue from 'vue';

const MyComponent = Vue.extend({
  data() {
    return {
      msg: 'Hello, World!'
    };
  },
  methods: {
    sayHello() {
      console.log(this.msg);
    }
  }
});

const app = new MyComponent().$mount('#app');

在这个例子中,MyComponent 继承自 Vue 构造函数。通过 Vue.extend 创建的组件其实是通过原型链继承了 Vue.prototype

Vue 的响应式系统利用了 Object.definePropertyProxy(Vue 3.x 中)来实现属性拦截和依赖追踪。这些响应式属性的 gettersetter 函数都定义在原型链上。

const data = {
  message: 'Hello Vue'
};

const reactiveHandler = {
  get(target, key) {
    console.log('Getting key:', key);
    return target[key];
  },
  set(target, key, value) {
    console.log('Setting key:', key, 'to', value);
    target[key] = value;
    return true;
  }
};

const proxy = new Proxy(data, reactiveHandler);

proxy.message; // 输出: Getting key: message
proxy.message = 'Hello React'; // 输出: Setting key: message to Hello React

在 Vue 中,当你定义一个响应式对象时,Vue 会在原型链上设置这些响应式属性的 gettersetter,确保数据变动能够被捕捉并引发视图更新。

尽管现代 JavaScript 框架如 React 和 Vue 主要使用类、组件化等更高层的抽象,但原型链依然是它们背后实现多态和继承关系的基础。理解原型链不仅帮助我们更好地理解这些框架的原理,还能提升我们调试代码和进行性能优化的能力。例如,通过明确框架如何在原型链上查找方法和属性,我们可以更有效地组织代码,避免不必要的属性查找和性能开销。

面试实战

题一:什么是原型链?

答案:原型链是一种对象属性和方法的查找机制。每个 JavaScript 对象都有一个隐藏的、内部的[[Prototype]]属性,通常作为 __proto__ 外部属性存在。对象的 __proto__ 指向它的原型对象,而这个原型对象又有它自己的原型,依此类推,直到一个对象的 __proto__null 为止。这种结构形成了一条链,称为原型链。

题二:如何创建对象的原型链?

答案:可以通过以下几种方式创建对象的原型链:

  • 使用构造函数和new关键字。
  • 使用Object.create()方法。
  • 使用class关键字(ES6新引入的语法糖,背后仍然使用原型链)。

题三:描述一下__proto__和prototype之间的区别?

答案:__proto__是每个JavaScript对象(除了null原型对象)都内置的一个属性,指向创建该对象的构造函数的原型。这个属性是用来构建原型链的。 prototype是函数对象特有的属性,只有函数对象才有prototype属性。当函数对象作为构造函数用于创建实例时,被创建的对象的__proto__属性将指向这个构造函数的prototype属性指向的对象。

题四:何时使用hasOwnProperty方法?

答案:hasOwnPropertyObject.prototype的一个方法,用于检查一个属性是否为对象自身的属性,而不是继承自原型链。这在遍历一个对象的属性时特别重要,以避免执行原型链上继承的属性。

题五:解释以下代码中的原型链结构。

function A() {}
function B() {}

A.prototype = new B();
const a = new A();

答案:上述代码创建的原型链结构如下:

  1. a__proto__ 指向 A.prototype
  2. A.prototypeB 的一个实例,因此 A.prototype.__proto__ 指向 B.prototype
  3. B.prototype__proto__ 指向 Object.prototype

题六:通过构造函数创建两个对象时,他们的原型是否相同?

答案:是的,如果两个对象是通过同一个构造函数创建的,则它们的原型对象是相同的。这是因为构造函数有一个 prototype 属性,所有由这个构造函数创建的对象实例都会共享这个原型对象。例如:

function Person() {}

var person1 = new Person();
var person2 = new Person();

console.log(person1.__proto__ === person2.__proto__); // true

在这里 person1person2 同时共享 Person.prototype

题七:解释以下代码的输出,并解释原因。

function Animal() {}
function Dog() {}

Dog.prototype = new Animal();

var myDog = new Dog();
console.log(myDog instanceof Animal);

答案:输出将是 true。因为 Dog.prototype 被设置为 Animal 的一个实例,所有由 Dog 构造的对象就自动拥有 Animal 在其原型链上。 instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个对象的原型链中。

题八:解释如何用原型链来实现私有方法?

答案:JavaScript 没有像其他面向对象语言那样的私有方法,但可以通过闭包和作用域链来模拟私有方法。

function MyClass() {
  this.publicMethod = function() {
    console.log('This is public');
    privateMethod();
  };

  const privateMethod = function() {
    console.log('This is private');
  }
}

const instance = new MyClass();
instance.publicMethod();  // 输出: This is public, This is private
instance.privateMethod(); // TypeError: instance.privateMethod is not a function