深入理解JavaScript-原型(一)

简介: 深入理解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);

打印之后:

image.png

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

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


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

image.png


这里需要说明:虽然在浏览器中打印 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

image.png

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

image.png

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

image.png

如果你眼熟这上面的属性,就能明白它是 Object.prototype,也就是说


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


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

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

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


相关文章
|
6月前
|
JavaScript 前端开发
js开发:请解释原型继承和类继承的区别。
JavaScript中的原型继承和类继承用于共享对象属性和方法。原型继承通过原型链实现共享,节省内存,但不支持私有属性。
54 0
|
6月前
|
JavaScript 前端开发 Java
深入JS面向对象(原型-继承)(三)
深入JS面向对象(原型-继承)
54 0
|
6月前
|
JavaScript 前端开发 Java
深入JS面向对象(原型-继承)(一)
深入JS面向对象(原型-继承)
61 0
|
3月前
|
JavaScript 前端开发
如何在JavaScript中实现基于原型的继承机制
【8月更文挑战第14天】如何在JavaScript中实现基于原型的继承机制
31 0
|
2月前
|
JSON JavaScript 前端开发
js原型继承|26
js原型继承|26
|
2月前
|
JavaScript 前端开发
JavaScript prototype(原型对象)
JavaScript prototype(原型对象)
32 0
|
2月前
|
JavaScript 前端开发
JavaScript基础知识-原型(prototype)
关于JavaScript基础知识中原型(prototype)概念的介绍。
38 1
|
3月前
|
JavaScript 前端开发
JavaScript中什么是原型?有什么用?
JavaScript中什么是原型?有什么用?
23 1
|
3月前
|
JavaScript 前端开发 Java
什么是JavaScript原型对象
【8月更文挑战第2天】什么是JavaScript原型对象
61 9
|
5月前
|
设计模式 JavaScript 前端开发
【JavaScript】深入浅出JavaScript继承机制:解密原型、原型链与面向对象实战攻略
JavaScript的继承机制基于原型链,它定义了对象属性和方法的查找规则。每个对象都有一个原型,通过原型链,对象能访问到构造函数原型上的方法。例如`Animal.prototype`上的`speak`方法可被`Animal`实例访问。原型链的尽头是`Object.prototype`,其`[[Prototype]]`为`null`。继承方式包括原型链继承(通过`Object.create`)、构造函数继承(使用`call`或`apply`)和组合继承(结合两者)。ES6的`class`语法是语法糖,但底层仍基于原型。继承选择应根据需求,理解原型链原理对JavaScript面向对象编程至关重要
137 7
【JavaScript】深入浅出JavaScript继承机制:解密原型、原型链与面向对象实战攻略