乐趣区

关于前端:深入理解JavaScript原型

这篇文章将尝试答复这些问题:

  • 原型是什么
  • 为什么要有原型
  • prototype 和 __proto__ 有什么区别
  • 原型链又是什么
  • 原型是如何实现继承的
  • 原型和原型链的关系如何

概述

首先,JavaScript 是基于原型继承(Prototypal inheritance)的语言。原型(prototype)是给其余对象提供共享属性的对象,每个函数都有一个 prototype 属性,它指向的是一个 prototype 对象。每个对象都有一个隐式援用([[Prototype]]),并且 [[Prototype]] 指向它的原型对象,并从中继承数据、构造和行为。同时原型对象同样领有原型(函数也是对象,它也有[[Prototype]]),这样一层一层,最终指向 null,这种关系被称为原型链

从实质上说,原型是为实现继承的伎俩。既然 JavaScript 抉择了这种形式实现,咱们就有必要探讨原型继承是什么?它有什么优缺点以及它与类继承的区别,以及在 JavaScript 中其余继承形式

在文章开始,咱们先对立一些概念问题,以便后文了解

对立概念

《JavaScript 高级程序设计第 4 版》介绍原型:

无论何时,只有创立一个函数,就会依照特定的规定为这个函数创立一个 prototype 属性(指向原型对象)。默认状况下,所有原型对象主动取得一个名为 constructor 的属性,指回与之关联的构造函数

《JavaScript 高级程序设计第 4 版》英文版介绍原型:

Whenever a function is created, its prototype property is also created according to a specific set of rules. By default, all prototypes automatically get a property called constructor that points back to the function on which it is a property.

ECMA 标准中如此定义原型:

4.4.8 prototype

object that provides shared properties for other objects

其被定义为:为其余对象提供共享属性的对象

MDN 介绍原型:

遵循 ECMAScript 规范,someObject.[[Prototype]] 符号是用于指向 someObject 的原型。从 ECMAScript 6 开始,[[Prototype]] 能够通过 Object.getPrototypeOf()Object.setPrototypeOf()拜访器来拜访。这个等同于 JavaScript 的非标准但许多浏览器实现的属性 __proto__

但它不应该与构造函数 funcprototype 属性相混同。被构造函数创立的实例对象的 [[Prototype]] 指向 funcprototype 属性。Object.prototype 属性示意 Object 的原型对象

所以咱们这样了解原型:

  • 又名 prototype,它的职责是给其它对象提供共享属性
  • 从数据结构的角度看,它就是个单向链表
  • 原型对象:每个函数都有一个 prototype 属性,这个属性是一个指针,指向一个对象,这个对象称为原型对象;每个对象都有一个 [[Prototype]] 属性,它同样是个指针,指向原型对象
  • 原型属性:每个函数都有一个 prototype 属性,唤为原型属性,指向原型对象
  • 所以原型、prototype、原型对象、原型属性其实是一个货色的不同称说,就像一个人在父母眼里是孩子,在儿女背后是爸妈,走在路上就是一个路人。当咱们称说这个货色为原型时,想表白的是它有什么作用;当咱们称它为原型对象时,是因为每个对象在其创立时会自带 [[Prototype]] 属性,并指向它;当咱们称它为原型属性时,是因为每个函数都会在创立时自带 prototype 属性,而且这个属性是个指针,指向了原型对象

除此之外,还有一些概念:

  • 函数对象:所有 Function(内置构造函数)的实例都是函数对象
  • 一般对象:函数对象除外的均为一般对象
  • 构造函数:又称结构器,英文名叫 constructor
  • 隐式原型:__proto__,又名[[Prototype]],它指向原型对象

如此,咱们对立了概念,接下来解释下什么是 prototype、为什么会有 prototype… 等等问题

名词解释

prototype

原型会让笔者想起《百年孤独》,一族六代人的家族故事。笔者至今还记得书中的一句话:
家族的第一个人被绑在树上,家族的最初一个人正被蚂蚁吃掉

笔者为什么看到原型会想起《百年孤独》呢?因为笔者常被原型、原型对象、prototype、__proto__、[[Prototype]] 等名词和概念搞晕,就好比《百年孤独》中的人物名,过段时间就分不清谁是谁了

无论是书还是标准,都有一个对原型的解释:

JavaScript 的每个函数都有一个 prototype 属性,它指向原型对象;每个对象都有一个 [[Prototype]] 属性,它指向原型对象

Give you an example

function Foo() {}
Foo.prototype.name = 'johan';
console.dir(Foo);
console.dir(Foo.prototype);

打印之后:

打印 Foo 的原型,看到了三个属性。而咱们只赋值了 name,为什么会多两个参数呢?

实际上,语言底层帮咱们实现了,无论是什么对象,只有一创立,就会自带 constructor 和 [[Prototype]]。而原型对象亦是对象,即 Foo.prototype 是对象,所以它也有 constructor 和 [[Prototype]]

当然,因为函数也是对象,所以它也有 constructor 和 [[Prototype]]

这里须要阐明:尽管在浏览器中打印 Foo 没看到 constructor 属性,但它的确存在,它指向 Function 内置构造函数

这里咱们能够确认一点,只有创立一个函数,函数就会自带 prototype 属性,它是个对象,并带有 [[Prototype]] 和 constructor。那什么是 [[Prototype]] 呢

[[Prototype]] 和 __proto__

前文例子中用 Foo.__proto__ 来打印日志,而不是用 Foo.[[Prototype]],而在打印 Foo 时,却看到隐式原型的名字是 [[Prototype]],然而在其余文章中能看到__proto__

实际上,无论是 [[Prototype]],还是 __proto__,指的都是同一个货色,在较早的文章中,为辨别原型,咱们叫它隐式原型。而它的呈现,是一个历史问题

官网 ECMAScript 规定了 prototype 是个隐式援用,然而民间浏览器开了口子,实现了一个属性 __proto__,让实例对象能够通过 __proto__ 拜访原型对象。再起初官网只好向事实抬头,将 __proto__ 属性纳入标准中。起初在 ECMAScript 2015 提出了 getPrototypeOf() 及 setPrototypeOf() 办法来获取 / 设置原型对象

至于 [[Prototype]],是在浏览器打印才显示的,它和 __proto__ 是一个含意,只是浏览器厂商换了个马甲。而且咱们能在开发者工具中查看到的 [[Prototype]](或 __proto__)是浏览器厂商成心渲染的一个虚构节点。实际上并不存在该对象

所以 [[Prototype]] 属性既不能被 for in 遍历,也不能被 Object.key(obj) 查找进去

在前文中咱们解释了每个对象都有 [[Prototype]] 属性,它指向原型对象,而原型对象也有本人的隐式援用([[Prototype]]),也有本人的原型对象,咱们能够了解为父类对象。它的作用是当你在拜访一个属性时,如果对象外部不存在这个属性,就会循着 [[Prototype]] 属性指向它的原型对象(父类对象)上查找,如果父类对象仍然没有这个值,就会沿着父类对象的 [[Prototype]] 往它的父类对象上查找。以此类推,直到找到 null 为止。这一层层的追溯查找过程,就形成了原型链

prototype chain

原型链是 prototype 和 [[Prototype]] 的联合造成的产物

Give you an example

function Person(name) {this.name = name;}
Person.prototype.sayName = function () {return this.name;};
var johan = new Person('johan');
console.log(johan.sayName()); // 'johan'
console.log(johan.toString()); // '[object Object]'

这里波及到继承、new 关键字,后文会再做阐明,这里假如你已明确根底概念

咱们创立了一个构造函数 Person,在它的原型上创立一个办法 sayName,new Person 实例对象 johan,此时的 johan 属性上惟一的值就是 name

当咱们应用办法 johan.sayName() 时,它在自有属性上找,找不到 sayName 办法,就沿着 [[Prototype]] 往它的原型对象上找,即 Person.prototype,在这里它找到了 sayName,调用它返回值

当咱们调用办法 johan.toString(),同样,自有属性上找,找不到沿着 [[Prototype]] 往它的原型对象上找,如上,还是找不到,就沿着 Person.prototype 的原型对象再往上找,即 Person.prototype.__proto__,在这里,找到了属性 toString,调用并返回值

如果你眼生这下面的属性,就能明确它是 Object.prototype,也就是说

Person.prototype.__proto__ === Object.prototype; // true

即构造函数 Person 的原型继承自 Object.prototype

这就是原型链的作用,所以说 JavaScript 是基于原型继承的语言

所以依附原型,就能实现继承,至于构造函数(constructor)它同样也能实现继承,不过是另一个话题了

constructor

前文中始终有应用构造函数,它的作用是初始化对象,为对象成员变量赋初始值

对于 constructor,咱们能衍生出 constructor 的始祖 Function(内置构造函数)与 原型的始祖 Object.prototype 的鸡生蛋和蛋生鸡问题,具体可看这篇——JavaScript 中的始皇(后续文章更新)

也能通过盗用构造函数来实现继承,更能通过与原型的联合,实现更多可能的继承,具体所有的办法笔者会写在这篇——继承(后续文章更新)

不过在理解继承之前,无妨先看看原型继承

创建对象和原型继承

在讲 Object 时,曾讲到对象的创立有三种办法,对象字面量、关键字 new、Object.create,而这三者都为原型继承

所谓的原型继承,无非是将一个对象设置为另一个对象的原型

在 JavaScript 中,有两类原型继承的办法:显式继承和隐式继承。两者的区别在于是否被动操作。像对象字面量、关键字 new 就是隐式继承,语言底层帮咱们做了继承(正如前文第一个例子),像 Object.create,就是显式继承,须要开发者手动操作。除此之外,还有一种显式继承——Object.setPrototypeOf

Object.setPrototypeOf

此办法设置一个特定的对象的原型到另一个对象或 null。ES6 新增办法。语法为:Object.setPrototypeOf(obj, prototype)。具体例子为:

const obja = {a: 1};
const objb = {b: 1};

Object.setPrototypeOf(obja, objb);
console.log(obja);

能看出,咱们通过 Object.setPrototypeOf 办法,将 objb 设置为 obja 的原型,打印 obja 时,咱们能看到 obja 的隐式原型([[Prototype]])指向 objb

除此之外,还有一个办法能显式继承原型——Object.create

Object.create

它用于创立一个新对象,应用现有的对象作为新创建对象的原型。ES5 新增办法。它的语法是 Object.create(proto, propertiesObject)。可看案例:

const obja = {a: 1};
const a = Object.create(obja);
console.log(a);

笔者在此之前曾写过一篇文章——Object.create,介绍它是如何实现的,其底层就是用到了原型继承

咱们对 Object.setPrototypeOf 和 Object.create 进行比照

  • Object.setPrototypeOf,给我两个对象,将其中一个设置为另一个的原型
  • Object.create,给我一个对象,将它作为我所创立的新对象的原型

从倒退的角度看,Object.create 是 ES5 呈现,但它不满足两个对象设置原型时,ES6 就提供了新的办法——Object.setPrototypeOf。但无论如何,它们都是起初因为个别需要而新增的 API,无奈与 JavaScript 一开始采纳的隐式继承相媲美

隐式原型继承

这里,咱们无妨复习一下 new 实现原理:

  1. 创立一个新对象
  2. 设置该对象的 [[Prototype]] 为构造函数 prototype 属性
  3. 将构造函数外部的 this 赋值给该对象
  4. 执行构造函数的外部代码
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创立的新对象

这就是隐式继承,只有咱们应用 new 创建对象,就会执行以上步骤,当然,用对象字面量是有一样的隐式操作

var obj = {}; // === new Object() 对象字面量
var arr = []; // === new Array() 数组字面量
function func() {} //  new Function(); 函数字面量
var str = '123'; // === new Srting('123') 字符串字面量
var bool = true; // === new Boolean(true) 布尔字面量
// ...

JavaScript 提供了几个内置的构造函数,如 Object、Array、Boolean、String、Number 等等,当咱们应用 {}[]function 等符号或关键字时,就是在执行与 new 操作一样的隐式原型继承

咱们能够这样说:隐式原型继承的目标是不便开发者更简洁的实现继承

隐式原型继承和显式原型继承的相互转换

无论是隐式原型继承,还是显式原型继承,都是对对象隐式援用的利用。两者之间具备肯定的互操作性,也就是说,用其中一个,能实现另一个的局部行为

例如咱们用隐式原型继承实现 Object.create,实现上是手写 Object.create 办法,正如咱们在 Object.create 中曾实现的那样:

function create(proto) {function F() {}
  F.prototype = proto;
  return new F();}

原理也很简略,创立一个函数,将它的原型赋值为指标对象,在实例化这个函数,返回实例化后的值。new 实例化,相当于实例化的值的 [[Prototype]] 指向了指标对象

function create(proto) {function F() {}
  // 创立一个函数,每个函数都有一个 prototype 属性,指向原型对象
  F.prototype = proto;
  // 本来的 prototype 是一个对象,其中有两个属性,一个是 constructor,即构造函数,指向 F;另一个为 [[Prototype]],指向 Object.prototype
  // 当初将它赋值为 proto
  return new F();
  // new 的作用是创立空对象,将该对象的原型赋值为另一个构造函数的 prototype 属性,并执行该构造函数
  // 所以 new F() 后的实例的的 [[Prototype]] 指向 F.prototype,也就是传入的 proto
}

以上,咱们就用 new 实现了显式原型继承

那么如何用显式原型继承实现 new(或者对象字面量)呢

咱们在手写 new 中曾经实现过,这里贴上代码:

function new2(Constructor, ...args) {var obj = Object.create(null); // 创立一个对象
  obj.__proto__ = Constructor.prototype; // 将新对象的 [[Prototype]] 属性赋值为构造函数的原型对象
  var result = Constructor.apply(obj.args); // this 赋值新对象并初始化
  return typeof result === 'object' ? result : obj; // 非空返回后果
}

这也解释了面试时常考的两个面试题:手写 new 和手写 Object.create,两者一个是隐式原型继承,另一个是显式原型继承,两者间能通过各自个性实现对方办法

总结

如此,咱们就明确了原型是什么,原型与原型链的关系等等。而且咱们晓得「JavaScript 是基于原型继承的语言」这句话的涵义。

尽管当初原型在面试中曾经显得不重要,知乎上曾有过这样的问题——面试一个 5 年的前端,却连原型链也搞不清楚,满口都是 Vue,React 之类的实现,这样的人该用吗?。不懂原型照样开发业务也很失常。笔者感觉这并不奇怪,毕竟前端的前端开发曾经从面向对象转向函数式编程

在文末答复下文章结尾的问题

Q:原型是什么?

A:给其余对象提供共享属性的对象

Q:为什么要有原型?

A:JavaScript 是基于原型继承的语言,在这里是为了实现继承

Q:prototype 和 __proto__ 有什么区别

A:prototype 是函数特有的属性,每个函数创立时都会自带 prototype 属性,它指向一个对象,这个对象叫做原型对象。而每个对象都有一个 __proto__ 属性,它也指向原型对象。如果说两者有什么关系?那么 子对象的__proto__ === 父对象的 prototype

Q:原型链又是什么

A:每个对象都有 __proto__ 属性,它指向原型对象,原型对象也是对象,也有 __proto__ 属性,并指向它的原型对象,这样一层一层,最终指向 null,这种关系被称为原型链。

Q:原型是如何实现继承的

A:原型继承有四种办法,以是否手动操作为根据分为隐式原型继承和显式原型继承。隐式原型继承在咱们开发中占大多数,即对象字面量和 new,即这两种办法语言底层会帮咱们实现创建对象、关联原型和属性初始化。显式原型继承分为 Object.create 和 Object.setPrototypeOf,它能被动设置某个对象为另一个对象的原型

Q:原型和原型链的关系如何

A:原型是实现继承的办法,原型链是继承的产物

参考资料

  • 深刻了解 JavaScript 原型
  • 如何答复面试中的 JavaScript 原型链问题

系列文章

  • 深刻了解 JavaScript- 开篇
  • 深刻了解 JavaScript-JavaScript 是什么
  • 深刻了解 JavaScript-JavaScript 由什么组成
  • 深刻了解 JavaScript- 所有皆对象
  • 深刻了解 JavaScript-Object(对象)
  • 深刻了解 JavaScript-new 做了什么
  • 深刻了解 JavaScript-Object.create
  • 深刻了解 JavaScript- 拷贝的机密
退出移动版