[JS] You Don't Know JavaScript [this & Object Prototypes] - Prototypes [上]

前言

我们在Object [上]Object [下]中多次提到[[Prototype]],但是都没有提到他是什麽,现在我们将在这个章节中介绍什麽是prototype。


[[Prototype]]

JavaScript中的物件都会拥有一个内部属性,在语言规范中称为[[Prototype]],它用於被另一个物件引用,在这些大部分物件刚被建立的时候,Prototype会被设定为非null

const myObject = {
    a: 2
};
myObject.a; // 2

我们在Object [下]中提到的

他会先在Object上执行一个[[Get]]操作,他会检查物件并在物件中寻找有没有这个被请求的属性名称,如果找到就返回相对应的值,如果没能在物件中找到属性则会做另一个重要的事情(遍历[prototype]链)

以上面的程序来说,myObject.a他会调用myObject中的[[Get]]方法来检查本身物件中是否有一个a属性,如果有就会直接使用它,而本身物件中没有这个属性的话才会将注意力转向物件的[[Prototype]],会顺着prototype链不断的向上寻找,直到找到这个属性或回传undefined(没找到)

const anotherObject = {
    a: 2
};

const myObject = Object.create(anotherObject);
myObject.a; // 2

Object.create() 指定其原型物件与属性,创建一个新物件。

上面的程序码中,我们先建立一个anotherObject物件并新增了a属性,并且myObject的prototype连接到anotherObject上,所以当我们在找myObject中的a属性时,他会先检查自身有没有这个属性,没有的话则会向上一层prototype(anotherObject)寻找,所以就算自身物件中没有a这个属性但可以利用prototype这个机制找到更上层的物件。

Object.prototype

每一个[[Prototype]]的终点是内建的Object.prototype,这个物件包含各种在Javascript中被使用的供通工具,因为Javascript中所有普通的物件都是Object.prototype的衍伸

Setting & Shadowing Properties

物件的prototype寻找属性的概念其实跟scope寻找变数的概念相似,他们都会因为已经找到需要找的属性(变数)而停止继续向上寻找,这就是Shadowing的概念。

myObject.foo = 'bar';

如果foo同时出现在myObject本身myObject的更高层[[Prototype]],那麽Javascript就只会访问自身的foo属性,因为自身的foo属性shadowing了上层所有的同名属性。

但如果foo不存在於自身物件而是存在於myObject[[Prototype]]的更高层时,可能会有三种情况:

  1. 如果foo在[[Prototype]]的高层某处被找到,而这个foo属性没有被设定为唯读(writable: false),则这个foo会被添加到myObject上并将这个层级以上的所有同名属性shadowing。
  2. 如果foo在[[Prototype]]的高层某处被找到,但这个foo被设定为唯读(writable: true),那麽这个属性不能添加到myObject上和不能改变这个属性的状况,如果在严格模式下运行则会直出错误否则会这个属性的添加会被忽视,继续往上层[[Prototype]]寻找。
  3. 如果foo在[[Prototype]]的高层某处被找到而且他是一个setter,那麽这个被找到的属性便会一直被调用,没有foo会被添加到myObject(不会shadowing)。

Class

由於Javascript没有class的概念,所以继承需要透过将[[Prototype]]链到另一个Object上,由於Javascript中没有Class的概念,所以会有一些模仿class的奇异行为,让我们来介绍一下这个奇怪的方式。

在默认情况下,所有函数都会拥有一个名为prototype公开且不可枚举的属性

function Foo() {
    // ...
}
Foo.prototype; //{constructor: f}

这个Object通常称为Foo的原型,通过调用new Foo()创建的每个Object都将把他们(创建出的Object)的[[Prototype]]链接到Foo的原型。

function Foo(){
    // ...
}

const a = new Foo();
Object.getPrototypeOf(a) === Foo.prototype; // true

当a透过new关键字创建时,就会将a这个Object的[[Prototype]]链接到Foo的原形上,在Javascript中没有像OOP语言中可以instances出多个物件,而是创建了一个新的Object并将他们内部的[[Prototype]]链接在一起,没有初始化一个Object也没有对他进行拷贝而是将他们链接在一起

Constructors

function Foo(){
    // ...
}
const a = new Foo();

在上面的例子中,因为我们使用了new关键字使我们的Foo看起来很像一个class,除了点之外因为对於Foo()这个操作方法来说,看起来很像在调用Contructors函数,综合这两点让我们的这个行为看起来很像在使用一个class。

function Foo(){
    // ...
}
Foo.prototype.constructor === Foo; // true

const a = new Foo();
a.constructor === Foo; // true

Foo.prototype Object在默认情况下获得了一个公开且不可枚举的属性constructor,这个属性是对於这个函数的引用,此外我们也可以看到透过new关键字创建出的a Object也拥有一个constructor属性,因为a是透过new创建的所以他的属性会指向创建他的函数(Foo)

Constructor Or Call?

在上面的程序码中,因为我们透过new关键字构建一个Object(a)所以很容易将Foo归类为是个constructor,但实际上Foo并不是一个constructor他只是一个普通的函数,但是这边会有一个很奇怪的事情发生,当在一个常规函数前加上new关键字时,这个函数的调用就会变为constructor call (not constructor)

function NothingSpecial() {
	console.log( "Don't mind me!" );
}

var a = new NothingSpecial(); // "Don't mind me!"

a; // NothingSpecial {}

上面的例子中NothingSpecial只是一个普通的函数,但是当他前面加上new关键字时他就会constructor一个Object,这个调用的行为是constructor call(构造函数调用)但函数本身并不是constructor。

不过也可以说,在JavaScript中,所有在函数前加上new的函数都是constructor

Functions aren't constructors, but function calls are "constructor calls" if and only if new is used.

Mechanics

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

Foo.prototype.myName = function() {
	return this.name;
};

var a = new Foo( "a" );
var b = new Foo( "b" );

a.myName(); // "a"
b.myName(); // "b"

在上面的范例中展现了两个class-orientation的技巧:

  1. 我们在this All Makes Sense Now! [上]中的New Binding中提到,透过new关键字会先建立一个新的object再将这个新object的[[Prototype]]链接到创建他的函数上,最後这个创建他的函数的this会指向这个新建立出的物件,所以Foo中的this会指向新创建出的a、b。
  2. 在Foo.prototype的链上新增一个method,由於new关键字创建出的object都会将[[Prototype]]链接上创建他的函数,所以a、b的都会拥有Foo.prototype上的myName这个method。

第一次看到第二点的时候可能会觉得在创建a,b的时候将Foo.prototype中的method复制给了a与b,但想一想我们一开始介绍的prototype的找寻方法,当我们呼叫了a物件中的myName method时,Javascript引擎会先在a Object中找是否有这个method,若没有的话则会往prototype上层寻找,由於a与Foo的prototype链接再一起,所以便会向上找Foo的prototype,在这里找到这个method便拿来使用。

Constructor Redux

我们在一开始提到constructor属性的话题,当时我们提到a.constructor === Foo; //ture这件事,让我们以为a拥有一个constructor属性并且这个它指向Foo,但这是不正确的。

在默认情况下使用new Foo()创建出的物件上才会存在Foo.prototype的.constructor属性,但是如果我们将Foo的prototype更改为其他Object时,这样这个被更改的prototype便不会出现在被创建出的Object上。

function Foo(){
    // ...
}
// change prptotype object
Foo.prototype = {
    a: 1 
}

const a = new Foo();
a.constructor === Foo; // false
a.constructor === Object; // true

由於a这个Object本身没有.constructor这个属性,所以沿着[[Prototype]]向上找到了Foo.prototype,但这个物件中也没有.constructor(被我们手动更改),所以他会继续向上找,找到最上层的Object.prototype,在这里找到了.constructor,於是将a中的属性指向Object。

所以可以发现其实.constructor并不是一个不可变得属性,他只是不可枚举但依然是可更改的(writable: true),可以在[[Prototype]]上用任何值添加或覆盖掉原本产生的constructor属性。

根据[[Get]]的算法只要在任何一个地方找到.constructor就会把拿来使用,这可能会跟你的预期大不相同,所以对於.constructor来说它是极度不安全的引用,应该尽量避免使用


结论

本篇章中介绍了JavaScript中的[[Prototype]]以及Class的概念,让我们总结一下:

  • JavaScript中的物件都会拥有一个内部属性,在语言规范中称为[[Prototype]]
  • 每一个[[Prototype]]的终点是内建的Object.prototype,Javascript中所有普通的物件都是Object.prototype的衍伸
  • 物件的prototype寻找属性的概念其实跟scope寻找变数的概念相似,他们都会因为已经找到需要找的属性(变数)而停止继续向上寻找。
  • Javascript没有class的概念,所以继承需要透过将[[Prototype]]链到另一个Object上。
  • 使用new关键字建立新的Object时,这个新的Object的Prototype会链接到建立他的函数原型上
  • 使用new关键字建立新的Object,没有初始化一个Object也没有对他进行拷贝而是将他们链接在一起
  • 在JavaScript中,在普通函数前面加上new关键字,此函数的调用行为会变成constructor call(构造函数调用)
  • .constructor并不是一个不可变得属性,他只是不可枚举但依然可更改的(writable: true)

参考文献:
You Don't Know JavaScript


<<:  治理结构(Governance Structure)- 审计委员会(Audit committee)

>>:  网路流量统计分析 NTOP 後继版本 NTOPNG for Windows 10 20H2 的版本安装设定教学 Ntopng Windows install

何谓工程(Engineering)?

Image Credit: City of Gastonia 工程是指运用知识和技能来理解和管理利...

Day10 标签

大家好我是乌木白,今天我们要讲解的是 tag 标签。 什麽是标签? 在Git中,标签其实和 Com...

015-登入

今天来简单聊个登入页面。 其实一个简单的登入页面,能思考的事情就很多了。 首先,先聊聊登入最基本的两...

[DAY 03]物品拍卖价格查询功能(1/4)

制作FF14 chatbot主要步骤如下: 功能需求 资料收集 资料前处理 chatbot功能能开发...

26.Computed vs Methods

比较下面两个用法: <!-- computed --> <div>{{ re...