《你不知道的javascript上卷》摘要(下)

翻回看過的書线衫,整理筆記凿可,方便溫故而知新。這是一本很不錯的書授账,分為兩部分枯跑,第一部分主要講解了作用域惨驶、閉包,第二部分主要講解this敛助、對象原型等知識點(diǎn)粗卜。

第一部分 作用域和閉包

第二部分 this和對象原型

第1章 關(guān)于this

1.1 為什么要用this

this 提供了一種更優(yōu)雅的方式來隱式“傳遞”一個對象引用,因此可以將 API 設(shè)計得更加簡潔并且易于復(fù)用辜腺。

1.2 誤解

1.2.1 指向自身

人們很容易把 this 理解成指向函數(shù)自身休建。

function foo(num) {
  console.log( "foo: " + num );
  // 記錄 foo 被調(diào)用的次數(shù)
  this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
  if (i > 5) {
    foo( i );
  }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被調(diào)用了多少次?
console.log( foo.count ); // 0 -- WTF?

函數(shù)內(nèi)部代碼this.count 中的 this 并不是指向那個函數(shù)對象评疗。

// 具名函數(shù)测砂,在它內(nèi)部可以使用 foo 來引用自身。
function foo() {
  foo.count = 4; // foo 指向它自身
}
// 回調(diào)函數(shù)沒有名稱標(biāo)識符(這種函數(shù)被稱為匿名函數(shù))百匆,因此無法從函數(shù)內(nèi)部引用自身砌些。
setTimeout( function(){
  // 匿名(沒有名字的)函數(shù)無法指向自身
}, 10 );

方法一:(回避了 this 的問題,并且完全依賴于變量 foo 的詞法作用域)

function foo(num) {
  console.log( "foo: " + num );
  // 記錄 foo 被調(diào)用的次數(shù)
  foo.count++;
}
foo.count=0
var i;
for (i=0; i<10; i++) {
  if (i > 5) {
    foo( i );
  }
console.log( foo.count ); // 4

方法二:(接受了 this加匈,沒有回避它)

function foo(num) {
  console.log( "foo: " + num );
  // 記錄 foo 被調(diào)用的次數(shù)
  // 注意存璃,在當(dāng)前的調(diào)用方式下(參見下方代碼),this 確實(shí)指向 foo
  this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
  if (i > 5) {
    // 使用 call(..) 可以確保 this 指向函數(shù)對象 foo 本身
    foo.call( foo, i );
  }
}
console.log( foo.count ); // 4
1.2.2 它的作用域

this 在任何情況下都不指向函數(shù)的詞法作用域雕拼。

function foo() {
  var a = 2;
  this.bar();
}
function bar() {
  console.log( this.a );
}
foo(); // ReferenceError: a is not defined
  • 這段代碼試圖通過 this.bar() 來引用 bar() 函數(shù)纵东。這是絕對不可能成功的。調(diào)用 bar() 最自然的方法是省略前面的 this啥寇,直接使用詞法引用標(biāo)識符偎球。
  • 還試圖使用 this 聯(lián)通 foo() 和 bar() 的詞法作用域,從而讓bar() 可以訪問 foo() 作用域里的變量 a辑甜。這是不可能實(shí)現(xiàn)的衰絮,你不能使用 this 來引用一個詞法作用域內(nèi)部的東西。

1.3 this到底是什么

this 是在運(yùn)行時進(jìn)行綁定的磷醋,并不是在編寫時綁定猫牡,它的上下文取決于函數(shù)調(diào)用時的各種條件。
當(dāng)一個函數(shù)被調(diào)用時邓线,會創(chuàng)建一個活動記錄(有時候也稱為執(zhí)行上下文)淌友。這個記錄會包含函數(shù)在哪里被調(diào)用(調(diào)用棧)、函數(shù)的調(diào)用方法褂痰、傳入的參數(shù)等信息亩进。this 就是記錄的其中一個屬性。

1.4 小結(jié)

this 既不指向函數(shù)自身也不指向函數(shù)的詞法作用域缩歪!
this 實(shí)際上是在函數(shù)被調(diào)用時發(fā)生的綁定归薛,它指向什么完全取決于函數(shù)在哪里被調(diào)用。


第2章 this全面解析

調(diào)用位置就是函數(shù)在代碼中被調(diào)用的位置(而不是聲明的位置)。

綁定規(guī)則

默認(rèn)綁定

獨(dú)立函數(shù)調(diào)用主籍∠捌叮可以把這條規(guī)則看作是無法應(yīng)用其他規(guī)則時的默認(rèn)規(guī)則。

  • 如果使用嚴(yán)格模式(strict mode)千元,那么全局對象將無法使用默認(rèn)綁定苫昌。
  • 只有 foo() 運(yùn)行在非 strict mode 下時,默認(rèn)綁定才能綁定到全局對象幸海;嚴(yán)格模式下與 foo()的調(diào)用位置無關(guān)
function foo() {
  "use strict";
  console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined
function foo() {
  console.log( this.a );
}
var a = 2;
(function(){
  "use strict";
  foo(); // 2
})();
隱式綁定

調(diào)用位置是否有上下文對象祟身,或者說是否被某個對象擁有或者包含。
當(dāng)函數(shù)引用有上下文對象時物独,隱式綁定規(guī)則會把函數(shù)調(diào)用中的 this 綁定到這個上下文對象袜硫。

function foo() {
  console.log( this.a );
}
var obj = {
  a: 2,
  foo: foo
};
obj.foo(); // 2

隱式丟失
一個最常見的 this 綁定問題就是被隱式綁定的函數(shù)會丟失綁定對象,也就是說它會應(yīng)用默認(rèn)綁定挡篓,從而把 this 綁定到全局對象或者 undefined 上婉陷。

function foo() {
  console.log( this.a );
}
var obj2 = {
  a: 42,
  foo: foo
};
var obj1 = {
  a: 2,
  obj2: obj2
};
obj1.obj2.foo(); // 42
// 雖然 bar 是 obj.foo 的一個引用,但是實(shí)際上官研,它引用的是 foo 函數(shù)本身
// 因此此時的bar() 其實(shí)是一個不帶任何修飾的函數(shù)調(diào)用秽澳,因此應(yīng)用了默認(rèn)綁定。
function foo() {
  console.log( this.a );
}
var obj = {
  a: 2,
  foo: foo
};
var bar = obj.foo; // 函數(shù)別名戏羽!
var a = "oops, global"; // a 是全局對象的屬性
bar(); // "oops, global"
顯式綁定

可以使用函數(shù)的 call(..) 和apply(..) 方法担神。
如果你傳入了一個原始值(字符串類型、布爾類型或者數(shù)字類型)來當(dāng)作 this 的綁定對象始花,這個原始值會被轉(zhuǎn)換成它的對象形式(也就是 new String(..)杏瞻、new Boolean(..) 或者new Number(..))。這通常被稱為“裝箱”衙荐。

顯式綁定仍然無法解決我們之前提出的丟失綁定問題。
但是顯式綁定的一個變種可以解決這個問題——硬綁定

function foo() {
  console.log( this.a );
}
var obj = {
  a:2
};
var bar = function() {
  foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// 硬綁定的 bar 不可能再修改它的 this
bar.call( window ); // 2

內(nèi)部手動調(diào)用了 foo.call(obj)浮创,因此強(qiáng)制把 foo 的 this 綁定到了 obj忧吟。無論之后如何調(diào)用函數(shù) bar,它總會手動在 obj 上調(diào)用 foo斩披。這種綁定是一種顯式的強(qiáng)制綁定溜族,因此我們稱之為硬綁定。

硬綁定的典型應(yīng)用場景就是:
(1)創(chuàng)建一個包裹函數(shù)垦沉,傳入所有的參數(shù)并返回接收到的所有值煌抒。
(2)創(chuàng)建一個可以重復(fù)使用的輔助函數(shù)。
由于硬綁定是一種非常常用的模式厕倍,所以在 ES5 中提供了內(nèi)置的方法Function.prototype.bind寡壮,它的用法如下:

// bind(..) 會返回一個硬編碼的新函數(shù),它會把參數(shù)設(shè)置為 this 的上下文并調(diào)用原始函數(shù)。
function foo(something) {
  console.log( this.a, something );
  return this.a + something;
}
var obj = {
  a:2
};
var bar = foo.bind( obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5

第三方庫的許多函數(shù)况既,以及 JavaScript 語言和宿主環(huán)境中許多新的內(nèi)置函數(shù)这溅,都提供了一個可選的參數(shù),通常被稱為“上下文”(context)棒仍,其作用和 bind(..) 一樣悲靴,確保你的回調(diào)函數(shù)使用指定的 this。這些函數(shù)實(shí)際上就是通過 call(..) 或者 apply(..) 實(shí)現(xiàn)了顯式綁定莫其,

new綁定
  • JavaScript 中 new 的機(jī)制實(shí)際上和面向類的語言完全不同癞尚。
  • 在 JavaScript 中,構(gòu)造函數(shù)只是一些使用 new 操作符時被調(diào)用的函數(shù)。它們并不會屬于某個類而钞,也不會實(shí)例化一個類仍秤。實(shí)際上,它們甚至都不能說是一種特殊的函數(shù)類型临燃,它們只是被 new 操作符調(diào)用的普通函數(shù)而已。
  • 實(shí)際上并不存在所謂的“構(gòu)造函數(shù)”烙心,只有對于函數(shù)的“構(gòu)造調(diào)用”膜廊。

使用 new 來調(diào)用函數(shù),或者說發(fā)生構(gòu)造函數(shù)調(diào)用時淫茵,會自動執(zhí)行下面的操作爪瓜。

  1. 創(chuàng)建(或者說構(gòu)造)一個全新的對象。
  2. 這個新對象會被執(zhí)行 [[ 原型 ]] 連接匙瘪。
  3. 這個新對象會綁定到函數(shù)調(diào)用的 this铆铆。
  4. 如果函數(shù)沒有返回其他對象,那么 new 表達(dá)式中的函數(shù)調(diào)用會自動返回這個新對象丹喻。
function foo(a) {
  this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2

使用 new 來調(diào)用 foo(..) 時薄货,我們會構(gòu)造一個新對象并把它綁定到 foo(..) 調(diào)用中的 this上。


*優(yōu)先級

  1. 函數(shù)是否在 new 中調(diào)用(new 綁定)碍论?如果是的話 this 綁定的是新創(chuàng)建的對象谅猾。var bar = new foo()
  2. 函數(shù)是否通過 call、apply(顯式綁定)或者硬綁定調(diào)用鳍悠?如果是的話税娜,this 綁定的是指定的對象。var bar = foo.call(obj2)
  3. 函數(shù)是否在某個上下文對象中調(diào)用(隱式綁定)藏研?如果是的話敬矩,this 綁定的是那個上下文對象。var bar = obj1.foo()
  4. 如果都不是的話蠢挡,使用默認(rèn)綁定弧岳。如果在嚴(yán)格模式下凳忙,就綁定到 undefined,否則綁定到全局對象缩筛。var bar = foo()

綁定例外

被忽略的this

如果你把 null 或者 undefined 作為 this 的綁定對象傳入 call消略、apply 或者 bind,這些值在調(diào)用時會被忽略瞎抛,實(shí)際應(yīng)用的是默認(rèn)綁定規(guī)則:

function foo(a,b) {
  console.log( "a:" + a + ", b:" + b );
}
// 把數(shù)組“展開”成參數(shù)艺演, 傳入null 占位值
foo.apply( null, [2, 3] ); // a:2, b:3
// 使用 bind(..) 進(jìn)行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3

總是使用 null 來忽略 this 綁定可能產(chǎn)生一些副作用。如果某個函數(shù)確實(shí)使用了this桐臊,那默認(rèn)綁定規(guī)則會把 this 綁定到全局對象胎撤,這將導(dǎo)致不可預(yù)計的后果(比如修改全局對象)。
一種“更安全”的做法是傳入一個空對象断凶,Object.create(null)

// 我們的 DMZ 空對象
var ? = Object.create( null );
// 把數(shù)組展開成參數(shù)
foo.apply( ?, [2, 3] ); // a:2, b:3
間接引用

間接引用最容易在賦值時發(fā)生:

function foo() {
  console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2

賦值表達(dá)式 p.foo = o.foo 的返回值是目標(biāo)函數(shù)的引用伤提,因此調(diào)用位置是 foo() 而不是p.foo() 或者 o.foo()。根據(jù)我們之前說過的认烁,這里會應(yīng)用默認(rèn)綁定肿男。

軟綁定

硬綁定這種方式可以把 this 強(qiáng)制綁定到指定的對象(除了使用 new時),防止函數(shù)調(diào)用應(yīng)用默認(rèn)綁定規(guī)則却嗡。問題在于舶沛,硬綁定會大大降低函數(shù)的靈活性,使用硬綁定之后就無法使用隱式綁定或者顯式綁定來修改 this窗价。
如果可以給默認(rèn)綁定指定一個全局對象和 undefined 以外的值如庭,那就可以實(shí)現(xiàn)和硬綁定相同的效果,同時保留隱式綁定或者顯式綁定修改 this 的能力撼港。

// 可以通過一種被稱為軟綁定的方法來實(shí)現(xiàn)我們想要的效果:
if (!Function.prototype.softBind) {
  Function.prototype.softBind = function(obj) {
    var fn = this;
    // 捕獲所有 curried 參數(shù)
    var curried = [].slice.call( arguments, 1 );
    var bound = function() {
      return fn.apply(
        (!this || this === (window || global)) ?
        obj : this
        curried.concat.apply( curried, arguments )
      );
    };
    bound.prototype = Object.create( fn.prototype );
    return bound;
  };
}

下面我們看看 softBind 是否實(shí)現(xiàn)了軟綁定功能:

function foo() {
  console.log("name: " + this.name);
}
var obj = { name: "obj" },obj2 = { name: "obj2" },obj3 = { name: "obj3" };
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看F核!帝牡!
fooOBJ.call( obj3 ); // name: obj3 <---- 看往毡!
setTimeout( obj2.foo, 10 );// name: obj <---- 應(yīng)用了軟綁定

可以看到,軟綁定版本的 foo() 可以手動將 this 綁定到 obj2 或者 obj3 上靶溜,但如果應(yīng)用默認(rèn)綁定卖擅,則會將 this 綁定到 obj。


this詞法

箭頭函數(shù)不使用 this 的四種標(biāo)準(zhǔn)規(guī)則墨技,而是根據(jù)外層(函數(shù)或者全局)作用域來決定 this。

如果你經(jīng)常編寫 this 風(fēng)格的代碼挎狸,但是絕大部分時候都會使用 self = this 或者箭頭函數(shù)來否定 this 機(jī)制扣汪,那你或許應(yīng)當(dāng):
1)只使用詞法作用域并完全拋棄錯誤 this 風(fēng)格的代碼;
2)完全采用 this 風(fēng)格锨匆,在必要時使用 bind(..)崭别,盡量避免使用 self = this 和箭頭函數(shù)冬筒。
當(dāng)然,包含這兩種代碼風(fēng)格的程序可以正常運(yùn)行茅主,但是在同一個函數(shù)或者同一個程序中混合使用這兩種風(fēng)格通常會使代碼更難維護(hù)舞痰,并且可能也會更難編寫。


總結(jié)

如果要判斷一個運(yùn)行中函數(shù)的 this 綁定诀姚,就需要找到這個函數(shù)的直接調(diào)用位置响牛。找到之后就可以順序應(yīng)用下面這四條規(guī)則來判斷 this 的綁定對象。

  1. 由 new 調(diào)用赫段?綁定到新創(chuàng)建的對象呀打。
  2. 由 call 或者 apply(或者 bind)調(diào)用?綁定到指定的對象糯笙。
  3. 由上下文對象調(diào)用贬丛?綁定到那個上下文對象。
  4. 默認(rèn):在嚴(yán)格模式下綁定到 undefined给涕,否則綁定到全局對象

第3章 對象

語法

對象可以通過兩種形式定義:聲明(文字)形式和構(gòu)造形式豺憔。

// 對象的文字語法大概是這樣:
var myObj = {
  key: value
  // ...
};
// 構(gòu)造形式大概是這樣:
var myObj = new Object();
myObj.key = value;

構(gòu)造形式和文字形式生成的對象是一樣的。唯一的區(qū)別是够庙,在文字聲明中你可以添加多個鍵 / 值對恭应,但是在構(gòu)造形式中你必須逐個添加屬性。


類型

在 JavaScript 中一共有六種主要類型(術(shù)語是“語言類型”):
string首启、number暮屡、booleannull毅桃、undefined褒纲、 object

簡單基本類型(string、boolean钥飞、number莺掠、null 和 undefined)本身并不是對象。null 有時會被當(dāng)作一種對象類型读宙,但是這其實(shí)只是語言本身的一個 bug彻秆,即對 null 執(zhí)行typeof null 時會返回字符串 "object"。 實(shí)際上结闸,null 本身是基本類型唇兑。
函數(shù)就是對象的一個子類型。數(shù)組也是對象的一種類型桦锄,具備一些額外的行為扎附。

內(nèi)置對象

JavaScript 中還有一些對象子類型,通常被稱為內(nèi)置對象结耀。有些內(nèi)置對象的名字看起來和簡單基礎(chǔ)類型一樣留夜,不過實(shí)際上它們的關(guān)系更復(fù)雜匙铡。
StringNumber碍粥、Boolean鳖眼、ObjectFunction嚼摩、Array钦讳、DateRegExp低斋、Error

var strPrimitive = "I am a string";
typeof strPrimitive; // "string"
strPrimitive instanceof String; // false
var strObject = new String( "I am a string" );
typeof strObject; // "object"
strObject instanceof String; // true
// 檢查 sub-type 對象
Object.prototype.toString.call( strObject ); // [object String]
  • 在必要時語言會自動把字符串字面量轉(zhuǎn)換成一個 String 對象蜂厅,引擎自動把字面量轉(zhuǎn)換成 String 對象。類似 42.359.toFixed(2) 的方法膊畴,引擎會把42 轉(zhuǎn)換成 new Number(42)掘猿。對于布爾字面量來說也是如此。
  • null 和 undefined 沒有對應(yīng)的構(gòu)造形式唇跨,它們只有文字形式稠通。
  • Date 只有構(gòu)造,沒有文字形式买猖。
  • 對于 Object改橘、Array、Function 和 RegExp(正則表達(dá)式)來說玉控,無論使用文字形式還是構(gòu)造形式飞主,它們都是對象,不是字面量高诺。
  • 由于這兩種形式都可以創(chuàng)建對象碌识,所以我們首選更簡單的文字形式。建議只在需要那些額外選項(xiàng)時使用構(gòu)造形式虱而。
  • Error 對象很少在代碼中顯式創(chuàng)建筏餐,一般是在拋出異常時被自動創(chuàng)建。也可以使用 new Error(..) 這種構(gòu)造形式來創(chuàng)建牡拇,不過一般來說用不著魁瞪。

內(nèi)容

  • 存儲在對象容器內(nèi)部的是這些屬性的名稱,它們就像指針(從技術(shù)角度來說就是引用)一樣惠呼,指向這些值真正的存儲位置导俘。
  • 如果要訪問 myObject 中 a 位置上的值,我們需要使用 . 操作符或者 [] 操作符剔蹋。.a 語法通常被稱為“屬性訪問”旅薄,["a"] 語法通常被稱為“鍵訪問”。[".."] 語法可以接受任意 UTF-8/Unicode 字符串作為屬性名滩租。
  • 在對象中赋秀,屬性名永遠(yuǎn)都是字符串。如果你使用 string以外的其他值作為屬性名律想,那它首先會被轉(zhuǎn)換為一個字符串猎莲。即使是數(shù)字也不例外。
  • ES6 增加了可計算屬性名技即,可以在文字形式中使用 [] 包裹一個表達(dá)式來當(dāng)作屬性名著洼。
var prefix = "foo";
var myObject = {
  [prefix + "bar"]:"hello",
  [prefix + "baz"]: "world"
};

屬性與方法

由于函數(shù)很容易被認(rèn)為是屬于某個對象,在其他語言中而叼,屬于對象(也被稱為“類”)的函數(shù)通常被稱為“方法”身笤,因此把“屬性訪問”說成是“方法訪問”也就不奇怪了。
從技術(shù)角度來說葵陵,函數(shù)永遠(yuǎn)不會“屬于”一個對象液荸。
無論返回值是什么類型,每次訪問對象的屬性就是屬性訪問脱篙。

function foo() {
  console.log( "foo" );
}
var someFoo = foo; // 對 foo 的變量引用
var myObject = {
  someFoo: foo
};
foo; // function foo(){..}
someFoo; // function foo(){..}
myObject.someFoo; // function foo(){..}

someFoo 和 myObject.someFoo 只是對于同一個函數(shù)的不同引用娇钱,并不能說明這個函數(shù)是特別的或者“屬于”某個對象。如果 foo() 定義時在內(nèi)部有一個 this 引用绊困,那這兩個函數(shù)引用的唯一區(qū)別就是 myObject.someFoo 中的 this 會被隱式綁定到一個對象文搂。


數(shù)組
  • 數(shù)組也支持 [] 訪問形式
  • 數(shù)組也是對象,所以雖然每個下標(biāo)都是整數(shù)秤朗,仍然可以給數(shù)組添加屬性
  • 雖然添加了命名屬性(無論是通過 . 語法還是 [] 語法)煤蹭,數(shù)組的 length 值并未發(fā)生變化

復(fù)制對象
  • 對于 JSON 安全的對象來說,有一種巧妙的復(fù)制方法:
    var newObj = JSON.parse( JSON.stringify( someObj ) );
  • 相比深復(fù)制取视,淺復(fù)制非常易懂并且問題要少得多硝皂,所以 ES6 定義了 Object.assign(..) 方法來實(shí)現(xiàn)淺復(fù)制。

屬性描述符
var myObject = {
a:2
};
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }

不僅僅只是一個 2贫途。它還包含另外三個特性:writable(可寫)吧彪、enumerable(可枚舉)和 configurable(可配置)。
可以使用 Object.defineProperty(..)來添加一個新屬性或者修改一個已有屬性(如果它是 configurable)并對特性進(jìn)行設(shè)置丢早。

// 一般來說你不會使用這種方式姨裸,除非你想修改屬性描述符。
var myObject = {};
Object.defineProperty( myObject, "a", {
  value: 2,
  writable: true,
  configurable: true,
  enumerable: true
} );
myObject.a; // 2

Writable
writable 決定是否可以修改屬性的值
Configurable

  • 只要屬性是可配置的怨酝,就可以使用 defineProperty(..) 方法來修改屬性描述符傀缩。
  • configurable 修改成false 是單向操作,無法撤銷农猬!
  • 即便屬性是 configurable:false赡艰,我們還是可以把 writable 的狀態(tài)由 true 改為 false,但是無法由 false 改為 true斤葱。
  • 除了無法修改慷垮,configurable:false 還會禁止刪除這個屬性

Enumerable
這個描述符控制的是屬性是否會出現(xiàn)在對象的屬性枚舉中揖闸,比如說for..in 循環(huán)。
用戶定義的所有的普通屬性默認(rèn)都是 enumerable料身。

不變性

所有的方法創(chuàng)建的都是淺不變性汤纸,也就是說,它們只會影響目標(biāo)對象和它的直接屬性芹血。如果目標(biāo)對象引用了其他對象(數(shù)組贮泞、對象、函數(shù)等)幔烛,其他對象的內(nèi)容不受影響啃擦,仍然是可變的。

  1. 對象常量
    結(jié)合 writable:false 和 configurable:false 就可以創(chuàng)建一個真正的常量屬性(不可修改饿悬、重定義或者刪除)
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
  value: 42,
  writable: false,
  configurable: false
} );
  1. 禁止擴(kuò)展
    禁止一個對象添加新屬性并且保留已有屬性令蛉,可以使用Object.preventExtensions:
var myObject = {
  a:2
};
Object.preventExtensions( myObject );
myObject.b = 3;
myObject.b; // undefined
  1. 密封
    Object.seal(..) 會創(chuàng)建一個“密封”的對象,這個方法實(shí)際上會在一個現(xiàn)有對象上調(diào)用Object.preventExtensions(..) 并把所有現(xiàn)有屬性標(biāo)記為configurable:false乡恕。
    所以言询,密封之后不僅不能添加新屬性,也不能重新配置或者刪除任何現(xiàn)有屬性(雖然可以修改屬性的值)傲宜。
  2. 凍結(jié)
    Object.freeze(..) 會創(chuàng)建一個凍結(jié)對象运杭,這個方法實(shí)際上會在一個現(xiàn)有對象上調(diào)用Object.seal(..) 并把所有“數(shù)據(jù)訪問”屬性標(biāo)記為 writable:false,這樣就無法修改它們的值函卒。這個方法是你可以應(yīng)用在對象上的級別最高的不可變性辆憔。
[[Get]]
var myObject = {
  a: 2
};
myObject.a; // 2
  • myObject.a 在 myObject 上實(shí)際上是實(shí)現(xiàn)了 [[Get]] 操作(有點(diǎn)像函數(shù)調(diào)
    用:[[Get]]())。
  • 對象默認(rèn)的內(nèi)置 [[Get]] 操作首先在對象中查找是否有名稱相同的屬性报嵌,如果找到就會返回這個屬性的值虱咧。
  • 如果沒有找到名稱相同的屬性,按照 [[Get]] 算法的定義會遍歷可能存在的 [[Prototype]] 鏈锚国,也就是原型鏈腕巡。
  • 如果都沒有找到名稱相同的屬性,那 [[Get]] 操作會返回值undefined血筑。
  • 這種方法和訪問變量時是不一樣的绘沉。如果你引用了一個當(dāng)前詞法作用域中不存在的變量,并不會像對象屬性一樣返回 undefined豺总,而是會拋出一個ReferenceError異常车伞。
[[Put]]

[[Put]] 被觸發(fā)時,實(shí)際的行為取決于許多因素喻喳,包括對象中是否已經(jīng)存在這個屬性(這是最重要的因素)另玖。
如果已經(jīng)存在這個屬性,[[Put]] 算法大致會檢查下面這些內(nèi)容。

  1. 屬性是否是訪問描述符谦去?如果是并且存在 setter 就調(diào)用 setter慷丽。
  2. 屬性的數(shù)據(jù)描述符中 writable 是否是 false ?如果是鳄哭,在非嚴(yán)格模式下靜默失敗盈魁,在嚴(yán)格模式下拋出 TypeError 異常。
  3. 如果都不是窃诉,將該值設(shè)置為屬性的值。
    如果對象中不存在這個屬性赤套,[[Put]] 操作會更加復(fù)雜飘痛。(后續(xù)介紹)
Getter和Setter

對象默認(rèn)的 [[Put]] 和 [[Get]] 操作分別可以控制屬性值的設(shè)置和獲取。
在 ES5 中可以使用 getter 和 setter 部分改寫默認(rèn)操作容握。

  • getter 是一個隱藏函數(shù)宣脉,會在獲取屬性值時調(diào)用。
  • setter 也是一個隱藏函數(shù)剔氏,會在設(shè)置屬性值時調(diào)用塑猖。
  • 當(dāng)你給一個屬性定義 getter、setter 或者兩者都有時谈跛,這個屬性會被定義為“訪問描述符”(和“數(shù)據(jù)描述符”相對)羊苟。
  • 對于訪問描述符來說,JavaScript 會忽略它們的 value 和writable 特性感憾,取而代之的是關(guān)心 set 和 get(還有configurable 和 enumerable)特性蜡励。
var myObject = {
// 給 a 定義一個 getter
get a() {
  return 2;
}
};
myObject.a = 3;
myObject.a; // 2

由于我們只定義了 a 的 getter,所以對 a 的值進(jìn)行設(shè)置時 set 操作會忽略賦值操作阻桅。應(yīng)當(dāng)定義 setter凉倚,和你期望的一樣,setter 會覆蓋單個屬性默認(rèn)的[[Put]](也被稱為賦值)操作嫂沉。通常來說 getter 和 setter 是成對出現(xiàn)的稽寒。

var myObject = {
  // 給 a 定義一個 getter
  get a() {
    return this._a_;
  },
   // 給 a 定義一個 setter
  set a(val) {
    this._a_ = val * 2;
  }
};
myObject.a = 2;
myObject.a; // 4
存在性

我們可以在不訪問屬性值的情況下判斷對象中是否存在這個屬性:

var myObject = {
  a:2
};
("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "b" ); // false
  • in 操作符會檢查屬性是否在對象及其 [[Prototype]] 原型鏈中。
  • hasOwnProperty(..) 只會檢查屬性是否在 myObject 對象中趟章,不會檢查 [[Prototype]] 鏈杏糙。
var myObject = { };
Object.defineProperty(
  myObject,
  "a",
  // 讓 a 像普通屬性一樣可以枚舉
  { enumerable: true, value: 2 }
);
Object.defineProperty(
  myObject,
  "b",
  // 讓 b 不可枚舉
  { enumerable: false, value: 3 }
);
myObject.b; // 3
("b" in myObject); // true
myObject.hasOwnProperty( "b" ); // true
// .......
for (var k in myObject) {
  console.log( k, myObject[k] );
}
// "a" 2

myObject.propertyIsEnumerable( "a" ); // true
myObject.propertyIsEnumerable( "b" ); // false
Object.keys( myObject ); // ["a"]
Object.getOwnPropertyNames( myObject ); // ["a", "b"]

  • myObject.b 確實(shí)存在并且有訪問值,但是卻不會出現(xiàn)在 for..in 循環(huán)中
  • 但可以通過in操作符來判斷是否存在(包括不可枚舉)尤揣,原因是“可枚舉”就相當(dāng)于“可以出現(xiàn)在對象屬性的遍歷中”搔啊。
  • propertyIsEnumerable(..) 會檢查給定的屬性名是否直接存在于對象中(而不是在原型鏈上)并且滿足 enumerable:true。
  • Object.keys(..) 會返回一個數(shù)組北戏,包含所有可枚舉屬性
  • Object.getOwnPropertyNames(..)會返回一個數(shù)組负芋,包含所有屬性,無論它們是否可枚舉。
  • in 和 hasOwnProperty(..) 的區(qū)別在于是否查找 [[Prototype]] 鏈旧蛾,然而莽龟,Object.keys(..)和 Object.getOwnPropertyNames(..) 都只會查找對象直接包含的屬性。

在數(shù)組上應(yīng)用 for..in 循環(huán)有時會產(chǎn)生出人意料的結(jié)果锨天,因?yàn)檫@種枚舉不僅會包含所有數(shù)值索引毯盈,還會包含所有可枚舉屬性。最好只在對象上應(yīng)用for..in 循環(huán)病袄,如果要遍歷數(shù)組就使用傳統(tǒng)的 for 循環(huán)來遍歷數(shù)值索引搂赋。

遍歷

for..in 循環(huán)可以用來遍歷對象的可枚舉屬性列表(包括 [[Prototype]] 鏈)。對于數(shù)值索引的數(shù)組來說益缠,可以使用標(biāo)準(zhǔn)的 for 循環(huán)來遍歷值脑奠,這實(shí)際上并不是在遍歷值,而是遍歷下標(biāo)來指向值幅慌。
ES5 中增加了一些數(shù)組的輔助迭代器宋欺,包括 forEach(..)、every(..) 和 some(..)胰伍。

  • forEach(..) 會遍歷數(shù)組中的所有值并忽略回調(diào)函數(shù)的返回值齿诞。
  • every(..) 會一直運(yùn)行直到回調(diào)函數(shù)返回 false(或者“假”值)。
  • some(..) 會一直運(yùn)行直到回調(diào)函數(shù)返回 true(或者“真”值)骂租。

遍歷對象屬性時的順序是不確定的祷杈,在不同的 JavaScript 引擎中可能不一樣。因此渗饮,在不同的環(huán)境中需要保證一致性時吠式,一定不要相信任何觀察到的順序,它們是不可靠的抽米。

ES6 增加了一種用來遍歷數(shù)組的 for..of 循環(huán)語法特占。for..of 循環(huán)首先會向被訪問對象請求一個迭代器對象,然后通過調(diào)用迭代器對象的next() 方法來遍歷所有返回值云茸。數(shù)組有內(nèi)置的 @@iterator是目,因此 for..of 可以直接應(yīng)用在數(shù)組上。和數(shù)組不同标捺,普通的對象沒有內(nèi)置的 @@iterator懊纳,所以無法自動完成 for..of 遍歷。

小結(jié)

  • JavaScript 中的對象有字面形式(比如 var a = { .. })和構(gòu)造形式(比如 var a = new Array(..))亡容。字面形式更常用嗤疯。
  • 對象就是鍵 / 值對的集合。訪問屬性時闺兢,引擎實(shí)際上會調(diào)用內(nèi)部的默認(rèn) [[Get]] 操作(在設(shè)置屬性值時是 [[Put]])茂缚,[[Get]] 操作會檢查對象本身是否包含這個屬性,如果沒找到的話還會查找 [[Prototype]]鏈。
  • 屬性的特性可以通過屬性描述符來控制脚囊,比如 writable 和 configurable龟糕。此外,可以使用Object.preventExtensions(..)悔耘、Object.seal(..) 和 Object.freeze(..) 來設(shè)置對象(及其屬性)的不可變性級別讲岁。
  • 屬性不一定包含值——它們可能是具備 getter/setter 的“訪問描述符”。
  • for..of會尋找內(nèi)置或者自定義的 @@iterator 對象并調(diào)用它的 next() 方法來遍歷數(shù)據(jù)值衬以。

第4章 混合對象“類”

4.1 類理論

  • 面向?qū)ο缶幊虖?qiáng)調(diào)的是數(shù)據(jù)和操作數(shù)據(jù)的行為本質(zhì)上是互相關(guān)聯(lián)的缓艳。好的設(shè)計就是把數(shù)據(jù)以及和它相關(guān)的行為打包(或者說封裝)起來。
  • 所有字符串都是 String 類的一個實(shí)例看峻,也就是說它是一個包裹郎任,包含字符數(shù)據(jù)和我們可以應(yīng)用在數(shù)據(jù)上的函數(shù)。
  • Car 的定義就是對通用 Vehicle 定義的特殊化备籽。
  • 類的另一個核心概念是多態(tài),這個概念是說父類的通用行為可以被子類用更特殊的行為重寫分井。
  • 實(shí)際上车猬,相對多態(tài)性 允許我們從重寫行為中引用基礎(chǔ)行為。
  • 父類和子類使用相同的方法名來表示特定的行為尺锚,從而讓子類重寫父類
  • 類并不是必須的編程基礎(chǔ)珠闰,而是一種可選的代碼抽象。
  • JavaScript 中實(shí)際上有類呢瘫辩?簡單來說:不是伏嗜。
  • 其他語言中的類和JavaScript中的“類”并不一樣。
  • 類是一種設(shè)計模式伐厌。

4.2 類的機(jī)制

如果你想打開一扇門承绸,那就必須接觸真實(shí)的建筑才行——藍(lán)圖只能表示門應(yīng)該在哪,但并不是真正的門挣轨。一個類就是一張藍(lán)圖军熏。為了獲得真正可以交互的對象,我們必須按照類來建造(也可以說實(shí)例化)一個東西卷扮,這個東西通常被稱為實(shí)例荡澎,有需要的話,我們可以直接在實(shí)例上調(diào)用方法并訪問其所有公有數(shù)據(jù)屬性晤锹。這個對象就是類中描述的所有特性的一份副本摩幔。

構(gòu)造函數(shù)

類實(shí)例是由一個特殊的類方法構(gòu)造的,這個方法名通常和類名相同鞭铆,被稱為構(gòu)造函數(shù)或衡。這個方法的任務(wù)就是初始化實(shí)例需要的所有信息(狀態(tài))。

class CoolGuy {
  specialTrick = nothing
  CoolGuy( trick ) {
    specialTrick = trick
  }
  showOff() {
    output( "Here's my trick: ", specialTrick )
  }
}
// 我們可以調(diào)用類構(gòu)造函數(shù)來生成一個 CoolGuy 實(shí)例:
Joe = new CoolGuy( "jumping rope" )
Joe.showOff() // 這是我的絕技:跳繩

CoolGuy 類有一個 CoolGuy() 構(gòu)造函數(shù),執(zhí)行 new CoolGuy() 時實(shí)際上調(diào)用的就是它薇宠。類構(gòu)造函數(shù)屬于類偷办,而且通常和類同名。此外澄港,構(gòu)造函數(shù)大多需要用 new 來調(diào)椒涯。

4.3 類的繼承

定義好一個子類之后,相對于父類來說它就是一個獨(dú)立并且完全不同的類回梧。子類會包含父類行為的原始副本废岂,但是也可以重寫所有繼承的行為甚至定義新行為。我們討論的父類和子類并不是實(shí)例狱意。應(yīng)當(dāng)把父類和子類稱為父類 DNA 和子類 DNA湖苞。

class Vehicle {
  engines = 1
  ignition() {
    output( "Turning on my engine." );
  }
  drive() {
    ignition();
    output( "Steering and moving forward!" )
  }
}
class Car inherits Vehicle {
  wheels = 4
  drive() {
    inherited:drive()
    output( "Rolling on all ", wheels, " wheels!" )
  }
}
class SpeedBoat inherits Vehicle {
  engines = 2
  ignition() {
    output( "Turning on my ", engines, " engines." )
  }
  pilot() {
  inherited:drive()
    output( "Speeding through the water with ease!" )
  }
}

Car 重寫了繼承自父類的 drive() 方法,但是之后 Car 調(diào)用了 inherited:drive() 方法详囤,這表明 Car 可以引用繼承來的原始 drive() 方法财骨。這個技術(shù)被稱為多態(tài)或者虛擬多態(tài)。在本例中藏姐,更恰當(dāng)?shù)恼f法是相對多態(tài)隆箩。

多態(tài)

任何方法都可以引用繼承層次中高層的方法(無論高層的方法名和當(dāng)前方法名是否相同)艺糜。之所以說“相對”是因?yàn)槲覀儾⒉粫x想要訪問的絕對繼承層次(或者說類)险胰,而是使用相對引用“查找上一層”。

在 pilot() 中通過相對多態(tài)引用了(繼承來的)Vehicle 中的 drive()茎杂。但是那個 drive() 方法直接通過名字(而不是相對引用)引用了 ignotion() 方法兜材。實(shí)際上它會使用SpeedBoat 的 ignition()理澎。ignition() 方法定義的多態(tài)性取決于你是在哪個類的實(shí)例中引用它。

在子類(而不是它們創(chuàng)建的實(shí)例對象J锕选)中也可以相對引用它繼承的父類糠爬,這種相對引用通常被稱為 super。
子類得到的僅僅是繼承自父類行為的一份副本举庶。子類對繼承到的一個方法進(jìn)行“重寫”秩铆,不會影響父類中的方法,這兩個方法互不影響灯变。
多態(tài)并不表示子類和父類有關(guān)聯(lián)殴玛,子類得到的只是父類的一份副本。類的繼承其實(shí)就是復(fù)制添祸。

多重繼承

有些面向類的語言允許你繼承多個“父類”滚粟。多重繼承意味著所有父類的定義都會被復(fù)制到子類中。
相比之下刃泌,JavaScript 要簡單得多:它本身并不提供“多重繼承”功能凡壤。

4.4 混入

JavaScript 中只有對象署尤,并不存在可以被實(shí)例化的“類”。一個對象并不會被復(fù)制到其他對象亚侠,它們會被關(guān)聯(lián)起來曹体。由于在其他語言中類表現(xiàn)出來的都是復(fù)制行為,因此 JavaScript 開發(fā)者也想出了一個方法來模擬類的復(fù)制行為硝烂,這個方法就是混入箕别。

顯示混入

在許多庫和框架中被稱為extend(..),但是為了方便理解我們稱之為 mixin(..)滞谢。

// 非常簡單的 mixin(..) 例子 :
function mixin( sourceObj, targetObj ) {
  for (var key in sourceObj) {
    // 只會在不存在的情況下復(fù)制
    if (!(key in targetObj)) {
      targetObj[key] = sourceObj[key];
    }
  }
  return targetObj;
}
var Vehicle = {
  engines: 1,
  ignition: function() {
    console.log( "Turning on my engine." );
  },
  drive: function() {
    this.ignition();
    console.log( "Steering and moving forward!" );
  }
};
var Car = mixin( Vehicle, {
  wheels: 4,
  drive: function() {
    Vehicle.drive.call( this );
    console.log("Rolling on all " + this.wheels + " wheels!");
  }
} );
  • 在 JavaScript 中不存在類串稀,Vehicle 和 Car 都是對象。
  • 從技術(shù)角度來說狮杨,函數(shù)實(shí)際上沒有被復(fù)制母截,復(fù)制的是函數(shù)引用。
  • Vehicle.drive.call( this )就是所說的顯式多態(tài)橄教。在之前的偽代碼中對應(yīng)的語句是 inherited:drive()清寇,我們稱之為相對多態(tài)。
  • 由于 Car 和Vehicle 中都有 drive() 函數(shù)护蝶,為了指明調(diào)用對象华烟,我們必須使用絕對(而不是相對)引用。我們通過名稱顯式指定 Vehicle 對象并調(diào)用它的 drive() 函數(shù)滓走。
  • 如果直接執(zhí)行 Vehicle.drive(),函數(shù)調(diào)用中的 this 會被綁定到 Vehicle 對象而不是Car 對象帽馋,因此搅方,我們會使用 .call(this)來確保 drive() 在 Car 對象的上下文中執(zhí)行。
  • 如果函數(shù) Car.drive() 的名稱標(biāo)識符并沒有和 Vehicle.drive() 重疊绽族,就不需要實(shí)現(xiàn)方法多態(tài)姨涡。由于存在標(biāo)識符重疊,所以必須使用更加復(fù)雜的顯式偽多態(tài)方法吧慢。
  • 應(yīng)當(dāng)盡量避免使用顯式偽多態(tài)涛漂,因?yàn)檫@樣做往往得不償失。

第二種混入函數(shù)检诗,先進(jìn)行復(fù)制然后對 Car 進(jìn)行特殊化的話匈仗,就可以跳過存在性檢查。不過這種方法并不好用并且效率更低逢慌,所以不如第一種方法常用悠轩。
由于兩個對象引用的是同一個函數(shù),因此這種復(fù)制(或者說混入)實(shí)際上并不能完全模擬面向類的語言中的復(fù)制攻泼。如果你修改了共享的函數(shù)對象火架,那 Vehicle 和 Car 都會受到影響鉴象。

注意,只在能夠提高代碼可讀性的前提下使用顯式混入何鸡,避免使用增加代碼理解難度或者讓對象關(guān)系更加復(fù)雜的模式纺弊。如果使用混入時感覺越來越困難,那或許你應(yīng)該停止使用它了骡男。

寄生繼承
顯式混入模式的一種變體被稱為“寄生繼承”淆游,它既是顯式的又是隱式的:

//“傳統(tǒng)的 JavaScript 類”Vehicle
function Vehicle() {
  this.engines = 1;
}
Vehicle.prototype.ignition = function() {
  console.log( "Turning on my engine." );
};
Vehicle.prototype.drive = function() {
  this.ignition();
  console.log( "Steering and moving forward!" );
};
function Car() {
  var car = new Vehicle(); // 首先,car 是一個 Vehicle
  car.wheels = 4; // 接著我們對 car 進(jìn)行定制
  var vehDrive = car.drive;  // 保存到 Vehicle::drive() 的特殊引用
// 重寫 Vehicle::drive()
  car.drive = function() {
    vehDrive.call( this );
    console.log("Rolling on all " + this.wheels + " wheels!");
  }
  return car;
}
var myCar = new Car();
myCar.drive();
// Turning on my engine.
// Steering and moving forward!
// Rolling on all 4 wheels!
隱式混入
var Something = {
  cool: function() {
    this.greeting = "Hello World";
    this.count = this.count ? this.count + 1 : 1;
  }
};
Something.cool();
Something.greeting; // "Hello World"
Something.count; // 1
var Another = {
  cool: function() {
    Something.cool.call( this ); // 隱式把 Something 混入 Another
  }
};
Another.cool();
Another.greeting; // "Hello World"
Another.count; // 1(count 不是共享狀態(tài))

通過在構(gòu)造函數(shù)調(diào)用或者方法調(diào)用中使用 Something.cool.call( this )洞翩,我們實(shí)際上“借用”了函數(shù) Something.cool() 并在 Another 的上下文中調(diào)用了它稽犁。最終的結(jié)果是 Something.cool() 中的賦值操作都會應(yīng)用在 Another 對象上而不是Something 對象上。

4.5 小結(jié)

  • 類是一種設(shè)計模式骚亿。JavaScript 也有類似的語法已亥,但是和其他語言中的類完全不同。
  • 類意味著復(fù)制来屠。
  • 傳統(tǒng)的類被實(shí)例化時虑椎,它的行為會被復(fù)制到實(shí)例中。類被繼承時俱笛,行為也會被復(fù)制到子類中捆姜。
  • 多態(tài)(在繼承鏈的不同層次名稱相同但是功能不同的函數(shù))看起來似乎是從子類引用父類,但是本質(zhì)上引用的其實(shí)是復(fù)制的結(jié)果迎膜。
  • 混入模式(無論顯式還是隱式)可以用來模擬類的復(fù)制行為泥技,但是通常會產(chǎn)生丑陋并且脆弱的語法。
  • 顯式混入實(shí)際上無法完全模擬類的復(fù)制行為磕仅,因?yàn)閷ο笾荒軓?fù)制引用珊豹,無法復(fù)制被引用的對象或者函數(shù)本身。

總地來說榕订,在 JavaScript 中模擬類是得不償失的店茶,雖然能解決當(dāng)前的問題,但是可能會埋下更多的隱患劫恒。

第5章 原型

5.1 [[Prototype]]

  • JavaScript 中的對象有一個特殊的 [[Prototype]] 內(nèi)置屬性贩幻,其實(shí)就是對于其他對象的引用。幾乎所有的對象在創(chuàng)建時 [[Prototype]] 屬性都會被賦予一個非空的值两嘴。
  • 當(dāng)你試圖引用對象的屬性時會觸發(fā)[[Get]] 操作丛楚。
  • 對于默認(rèn)的 [[Get]] 操作來說,如果無法在對象本身找到需要的屬性憔辫,就會繼續(xù)訪問對象的 [[Prototype]] 鏈鸯檬。
  • for..in 遍歷對象時原理和查找 [[Prototype]] 鏈類似,任何可以通過原型鏈訪問到(并且是 enumerable螺垢,參見第 3 章)的屬性都會被枚舉喧务。使用 in 操作符來檢查屬性在對象中是否存在時赖歌,同樣會查找對象的整條原型鏈(無論屬性是否可枚舉)。
  • 所有普通的 [[Prototype]] 鏈最終都會指向內(nèi)置的 Object.prototype功茴。
屬性設(shè)置和屏蔽
myObject.foo = "bar";

給一個對象設(shè)置屬性并不僅僅是添加一個新屬性或者修改已有的屬性值÷耄現(xiàn)在我們完整地講解一下這個過程:
(1)myObject 對象中包含名為 foo 的普通數(shù)據(jù)訪問屬性,只會修改已有的屬性值坎穿。
(2)foo 不是直接存在于 myObject 中展父,[[Prototype]] 鏈就會被遍歷,類似 [[Get]] 操作玲昧。如果原型鏈上找不到 foo栖茉,foo 就會被直接添加到 myObject 上。
(3)foo 存在于原型鏈上層:
①foo為普通數(shù)據(jù)訪問屬性且沒有被標(biāo)記為只讀(writable:false)孵延,會直接在 myObject 中添加一個名為 foo 的新屬性吕漂,它是屏蔽屬性。
②foo被標(biāo)記為只讀(writable:false)尘应,那么無法修改已有屬性或者在 myObject 上創(chuàng)建屏蔽屬性惶凝。不會發(fā)生屏蔽。
③foo是一個 setter犬钢,那就一定會調(diào)用這個 setter苍鲜。foo 不會被添加到(或者說屏蔽于)myObject,也不會重新定義 foo 這個 setter玷犹。
(4)屬性名 foo 既出現(xiàn)在 myObject 中也出現(xiàn)在 myObject 的 [[Prototype]] 鏈上層混滔,那么就會發(fā)生屏蔽,會屏蔽原型鏈上層的所有 foo 屬性歹颓。


var anotherObject = {
  a:2
};
var myObject = Object.create( anotherObject );
anotherObject.a; // 2
myObject.a; // 2
anotherObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false
myObject.a++; // 隱式屏蔽坯屿!
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty( "a" ); // true

++ 操作相當(dāng)于 myObject.a = myObject.a + 1。因此 ++ 操作首先會通過[[Prototype]]查找屬性 a 并從 anotherObject.a 獲取當(dāng)前屬性值 2晴股,然后給這個值加 1愿伴,接著用 [[Put]]將值 3 賦給 myObject 中新建的屏蔽屬性 a肺魁。


5.2 類

JavaScript并沒有類來作為對象的抽象模式或者說藍(lán)圖电湘。JavaScript 中只有對象。
JavaScript是少有的可以不通過類鹅经,直接創(chuàng)建對象的語言寂呛。對象直接定義自己的行為。

5.2.1 "類"函數(shù)

JavaScript 中有一種奇怪的行為一直在被無恥地濫用瘾晃,那就是模仿類贷痪。這種奇怪的“類似類”的行為利用了函數(shù)的一種特殊特性:所有的函數(shù)默認(rèn)都會擁有一個名為 prototype 的公有并且不可枚舉的屬性,它會指向另一個對象蹦误。

function Foo() {
// ...
}
Foo.prototype; // { }

這個對象是在調(diào)用 new Foo()時創(chuàng)建的劫拢,最后會被(有點(diǎn)武斷地)關(guān)聯(lián)到這個“Foo 點(diǎn) prototype”對象上肉津。

function Foo() {
// ...
}
var a = new Foo();
Object.getPrototypeOf( a ) === Foo.prototype; // true

調(diào)用 new Foo() 時會創(chuàng)建 a,其中的一步就是給 a 一個內(nèi)部的[[Prototype]] 鏈接舱沧,關(guān)聯(lián)到 Foo.prototype 指向的那個對象妹沙。
在面向類的語言中,類可以被復(fù)制(或者說實(shí)例化)多次熟吏。在 JavaScript 中距糖,并沒有類似的復(fù)制機(jī)制。只能創(chuàng)建多個對象牵寺,它們 [[Prototype]] 關(guān)聯(lián)的是同一個對象悍引。

  • new Foo() 會生成一個新對象(我們稱之為 a),這個新對象的內(nèi)部鏈接 [[Prototype]] 關(guān)聯(lián)的是 Foo.prototype 對象帽氓。
  • 最后我們得到了兩個對象趣斤,它們之間互相關(guān)聯(lián)。我們并沒有初始化一個類杏节,實(shí)際上我們并沒有從“類”中復(fù)制任何行為到一個對象中唬渗,只是讓兩個對象互相關(guān)聯(lián)。
  • new Foo() 這個函數(shù)調(diào)用實(shí)際上并沒有直接創(chuàng)建關(guān)聯(lián)奋渔,這個關(guān)聯(lián)只是一個意外的副作用镊逝。new Foo() 只是間接完成了我們的目標(biāo):一個關(guān)聯(lián)到其他對象的新對象。
  • 在 JavaScript 中嫉鲸,我們并不會將一個對象(“類”)復(fù)制到另一個對象(“實(shí)例”)撑蒜,只是將它們關(guān)聯(lián)起來。這個機(jī)制通常被稱為原型繼承玄渗。
  • 繼承意味著復(fù)制操作座菠,JavaScript(默認(rèn))并不會復(fù)制對象屬性。相反藤树,JavaScript 會在兩個對象之間創(chuàng)建一個關(guān)聯(lián)浴滴,這樣一個對象就可以通過委托訪問另一個對象的屬性和函數(shù)。委托這個術(shù)語可以更加準(zhǔn)確地描述 JavaScript 中對象的關(guān)聯(lián)機(jī)制岁钓。
  • 差異繼承升略。基本原則是在描述對象行為時屡限,使用其不同于普遍描述的特質(zhì)品嚣。
5.2.2 “構(gòu)造函數(shù)”
function Foo() {
// ...
}
Foo.prototype.constructor === Foo; // true
var a = new Foo();
a.constructor === Foo; //

到底是什么讓我們認(rèn)為 Foo 是一個“類”呢?其中一個原因是我們看到了關(guān)鍵字 new钧大,另一個原因是翰撑,看起來我們執(zhí)行了類的構(gòu)造函數(shù)方法。

  • Foo.prototype 默認(rèn)有一個公有并且不可枚舉的屬性 .constructor啊央,這個屬性引用的是對象關(guān)聯(lián)的函數(shù)眶诈。此外涨醋,通過“構(gòu)造函數(shù)”調(diào)用 new Foo() 創(chuàng)建的對象也有一個 .constructor 屬性,指向“創(chuàng)建這個對象的函數(shù)”逝撬。
  • 對于JavaScript 引擎來說首字母大寫沒有任何意義东帅。
  • 函數(shù)本身并不是構(gòu)造函數(shù),然而球拦,當(dāng)你在普通的函數(shù)調(diào)用前面加上 new 關(guān)鍵字之后靠闭,就會把這個函數(shù)調(diào)用變成一個“構(gòu)造函數(shù)調(diào)用”。實(shí)際上坎炼,new 會劫持所有普通函數(shù)并用構(gòu)造對象的形式來調(diào)用它愧膀。
  • 在 JavaScript 中對于“構(gòu)造函數(shù)”最準(zhǔn)確的解釋是,所有帶 new 的函數(shù)調(diào)用谣光。
  • 函數(shù)不是構(gòu)造函數(shù)檩淋,但是當(dāng)且僅當(dāng)使用 new 時,函數(shù)調(diào)用會變成“構(gòu)造函數(shù)調(diào)用”萄金。
5.2.3 技術(shù)
  • 看起來 a.constructor === Foo 為真意味著 a 確實(shí)有一個指向 Foo 的.constructor 屬性蟀悦,但是事實(shí)不是這樣。
  • 實(shí)際上氧敢,.constructor 引用同樣被委托給了 Foo.prototype日戈,而Foo.prototype.constructor 默認(rèn)指向 Foo。
  • a.constructor 只是通過默認(rèn)的 [[Prototype]] 委托指向 Foo孙乖,這和“構(gòu)造”毫無關(guān)系浙炼。
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 創(chuàng)建一個新原型對象
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!

a1 并沒有 .constructor 屬性,所以它會委托 [[Prototype]] 鏈上的 Foo.prototype唯袄。但是這個對象也沒有 .constructor 屬性弯屈,所以它會繼續(xù)委托,這次會委托給委托鏈頂端的 Object.prototype恋拷。這個對象有 .constructor 屬性资厉,指向內(nèi)置的 Object(..) 函數(shù)。
.constructor 并不是一個不可變屬性蔬顾。它是不可枚舉的宴偿,但是它的值是可寫的(可以被修改)。
a1.constructor 是一個非常不可靠并且不安全的引用阎抒。通常來說要盡量避免使用這些引用酪我。

5.3 (原型)繼承

下面這段代碼使用的就是典型的“原型風(fēng)格”:

function Foo(name) {
  this.name = name;
}
Foo.prototype.myName = function() {
  return this.name;
};
function Bar(name,label) {
  Foo.call( this, name );
  this.label = label;
}
// 我們創(chuàng)建了一個新的 Bar.prototype 對象并關(guān)聯(lián)到 Foo.prototype
Bar.prototype = Object.create( Foo.prototype );
// 注意消痛!現(xiàn)在沒有 Bar.prototype.constructor 了
// 如果你需要這個屬性的話可能需要手動修復(fù)一下它
Bar.prototype.myLabel = function() {
  return this.label;
};
var a = new Bar( "a", "obj a" );
a.myName(); // "a"
a.myLabel(); // "obj a"

調(diào)用Object.create(..) 會憑空創(chuàng)建一個“新”對象并把新對象內(nèi)部的 [[Prototype]] 關(guān)聯(lián)到你指定的對象(本例中是 Foo.prototype)且叁。Bar 會有一個 .prototype 關(guān)聯(lián)到默認(rèn)的對象,但是這個對象并不是我們想要的 Foo.prototype秩伞。因此我們創(chuàng)建了一個新對象并把它關(guān)聯(lián)到我們希望的對象上逞带,直接把原始的關(guān)聯(lián)對象拋棄掉欺矫。

// 并不會創(chuàng)建一個關(guān)聯(lián)到 Bar.prototype 的新對象
Bar.prototype = Foo.prototype;
// 基本上滿足你的需求,但是可能會產(chǎn)生一些副作用(比如寫日志展氓、修改狀態(tài)穆趴、注冊到其他對象、給 this 添加數(shù)據(jù)屬性遇汞,等等)
// 就會影響到 Bar() 的“后代”
Bar.prototype = new Foo();

要創(chuàng)建一個合適的關(guān)聯(lián)對象未妹,我們必須使用 Object.create(..) 而不是使用具有副作用的 Foo(..)。這樣做唯一的缺點(diǎn)就是需要創(chuàng)建一個新對象然后把舊對象拋棄掉空入,不能直接修改已有的默認(rèn)對象络它。
如果能有一個標(biāo)準(zhǔn)并且可靠的方法來修改對象的 [[Prototype]] 關(guān)聯(lián)就好了。ES6 添加了輔助函數(shù) Object.setPrototypeOf(..)歪赢,可以用標(biāo)準(zhǔn)并且可靠的方法來修改關(guān)聯(lián)化戳。

// ES6 之前需要拋棄默認(rèn)的 Bar.prototype
Bar.ptototype = Object.create( Foo.prototype );
// ES6 開始可以直接修改現(xiàn)有的 Bar.prototype
Object.setPrototypeOf( Bar.prototype, Foo.prototype );

檢查一個實(shí)例(JavaScript 中的對象)的繼承祖先(JavaScript 中的委托關(guān)聯(lián))通常被稱為內(nèi)省(或者反射)埋凯。

function Foo() {
// ...
}
Foo.prototype.blah = ...;
var a = new Foo();

我們?nèi)绾瓮ㄟ^內(nèi)省找出 a 的“祖先”(委托關(guān)聯(lián))呢点楼?第一種方法是站在“類”的角度來判斷:

a instanceof Foo; // true
  • instanceof 操作符的左操作數(shù)是一個普通的對象,右操作數(shù)是一個函數(shù)白对。instanceof 回答的問題是:在 a 的整條 [[Prototype]] 鏈中是否有指向Foo.prototype 的對象掠廓?
  • 如果想判斷兩個對象(比如 a 和 b)之間是否通過 [[Prototype]] 鏈關(guān)聯(lián),只用 instanceof無法實(shí)現(xiàn)甩恼。
// 是第二種判斷 [[Prototype]] 反射的方法却盘,它更加簡潔:
Foo.prototype.isPrototypeOf( a ); // true

// 非常簡單:b 是否出現(xiàn)在 c 的 [[Prototype]] 鏈中?
b.isPrototypeOf( c );

isPrototypeOf(..) 回答的問題是:在 a 的整條 [[Prototype]] 鏈中是否出現(xiàn)過 Foo.prototype 媳拴?這個方法并不需要使用函數(shù)(“類”)黄橘,它直接使用 b 和 c 之間的對象引用來判斷它們的關(guān)系。

獲取一個對象的 [[Prototype]] 鏈:

// 在 ES5 中屈溉,標(biāo)準(zhǔn)的方法是:
Object.getPrototypeOf( a );

// 絕大多數(shù)(不是所有H亍)瀏覽器也支持一種非標(biāo)準(zhǔn)的方法來訪問內(nèi)部 [[Prototype]] 屬性:
a.__proto__ === Foo.prototype; // true

和我們之前說過的 .constructor 一樣,.__proto__實(shí)際上并不存在于你正在使用的對象中(本例中是 a)子巾。實(shí)際上帆赢,它和其他的常用函數(shù)一樣,存在于內(nèi)置的 Object.prototype 中线梗。
.__proto__看起來很像一個屬性椰于,但是實(shí)際上它更像一個getter/setter。.__proto__的實(shí)現(xiàn)大致上是這樣的:

Object.defineProperty( Object.prototype, "__proto__", {
  get: function() {
    return Object.getPrototypeOf( this );
  },
  set: function(o) {
    // ES6 中的 setPrototypeOf(..)
    Object.setPrototypeOf( this, o );
    return o;
  }
} );

雖然 getter 函數(shù)存在于 Object.prototype 對象中仪搔,但是它的 this 指向?qū)ο?a瘾婿,所以和 Object.getPrototypeOf( a ) 結(jié)果相同。

5.4 對象關(guān)聯(lián)

[[Prototype]] 機(jī)制就是存在于對象中的一個內(nèi)部鏈接,它會引用其他對象偏陪。通常來說抢呆,這個鏈接的作用是:如果在對象上沒有找到需要的屬性或者方法引用,引擎就會繼續(xù)在 [[Prototype]] 關(guān)聯(lián)的對象上進(jìn)行查找笛谦。同理抱虐,如果在后者中也沒有找到需要的引用就會繼續(xù)查找它的[[Prototype]],以此類推饥脑。這一系列對象的鏈接被稱為“原型鏈”恳邀。

5.4.1 創(chuàng)建關(guān)聯(lián)

[[Prototype]] 機(jī)制的意義是什么呢?Object.create(..) 會創(chuàng)建一個新對象并把它關(guān)聯(lián)到我們指定的對象灶轰。我們并不需要類來創(chuàng)建兩個對象之間的關(guān)系轩娶,只需要通過委托來關(guān)聯(lián)對象就足夠了。而Object.create(..) 可以完美地創(chuàng)建我們想要的關(guān)聯(lián)關(guān)系框往。
Object.create(..) 是在 ES5 中新增的函數(shù)鳄抒。

5.4.2 關(guān)聯(lián)關(guān)系是備用
ar anotherObject = {
  cool: function() {
    console.log( "cool!" );
  }
};
var myObject = Object.create( anotherObject );
myObject.cool(); // "cool!"

當(dāng)你給開發(fā)者設(shè)計軟件時,假設(shè)要調(diào)用 myObject.cool()椰弊,如果 myObject 中不存在 cool()時這條語句也可以正常工作的話许溅,那你的 API 設(shè)計就會變得很“神奇”,對于未來維護(hù)你軟件的開發(fā)者來說這可能不太好理解秉版。

var anotherObject = {
  cool: function() {
    console.log( "cool!" );
  }
};
var myObject = Object.create( anotherObject );
myObject.doCool = function() {
  this.cool(); // 內(nèi)部委托贤重!
};
myObject.doCool(); // "cool!"

從內(nèi)部來說,我們的實(shí)現(xiàn)遵循的是委托設(shè)計模式清焕,通過 [[Prototype]] 委托到 anotherObject.cool()并蝗。

5.5 小結(jié)

  • 如果要訪問對象中并不存在的一個屬性,[[Get]] 操作(參見第 3 章)就會查找對象內(nèi)部[[Prototype]] 關(guān)聯(lián)的對象秸妥。這個關(guān)聯(lián)關(guān)系實(shí)際上定義了一條“原型鏈”(有點(diǎn)像嵌套的作用域鏈)滚停,在查找屬性時會對它進(jìn)行遍歷。
  • 所有普通對象都有內(nèi)置的 Object.prototype粥惧,指向原型鏈的頂端键畴。
  • 關(guān)聯(lián)兩個對象最常用的方法是使用 new 關(guān)鍵詞進(jìn)行函數(shù)調(diào)用。
  • 使用 new 調(diào)用函數(shù)時會把新對象的 .prototype 屬性關(guān)聯(lián)到“其他對象”突雪。帶 new 的函數(shù)調(diào)用通常被稱為“構(gòu)造函數(shù)調(diào)用”起惕。
  • JavaScript 中的機(jī)制有一個核心區(qū)別,那就是不會進(jìn)行復(fù)制咏删,對象之間是通過內(nèi)部的[[Prototype]] 鏈關(guān)聯(lián)的惹想。
  • 對象之間的關(guān)系不是復(fù)制而是委托。

第6章 行為委托

  • [[Prototype]] 機(jī)制就是指對象中的一個內(nèi)部鏈接引用另一個對象督函。
  • 如果在第一個對象上沒有找到需要的屬性或者方法引用嘀粱,引擎就會繼續(xù)在 [[Prototype]]關(guān)聯(lián)的對象上進(jìn)行查找激挪。同理,如果在后者中也沒有找到需要的引用就會繼續(xù)查找它的[[Prototype]]草穆,以此類推。這一系列對象的鏈接被稱為“原型鏈”搓译。
  • 換句話說悲柱,JavaScript 中這個機(jī)制的本質(zhì)就是對象之間的關(guān)聯(lián)關(guān)系。

面向委托的設(shè)計

類理論
class Task {
  id;
  // 構(gòu)造函數(shù) Task()
  Task(ID) { id = ID; }
  outputTask() { output( id ); }
}
class XYZ inherits Task {
  label;
  // 構(gòu)造函數(shù) XYZ()
  XYZ(ID,Label) { super( ID ); label = Label; }
  outputTask() { super(); output( label ); }
}
class ABC inherits Task {
  // ...
}
委托理論
Task = {
  setID: function(ID) { this.id = ID; },
  outputID: function() { console.log( this.id ); }
};
// 讓 XYZ 委托 Task
XYZ = Object.create( Task );
XYZ.prepareTask = function(ID,Label) {
  this.setID( ID );
  this.label = Label;
};
XYZ.outputTaskDetails = function() {
  this.outputID();
  console.log( this.label );
};
// ABC = Object.create( Task );
// ABC ... = ...

Task 和 XYZ 并 不 是 類( 或 者 函 數(shù) )些己, 它 們 是 對 象豌鸡。XYZ 通 過 Object.create(..) 創(chuàng)建,它的 [[Prototype]] 委托了 Task 對象段标。
相比于面向類(或者說面向?qū)ο螅┭墓冢視堰@種編碼風(fēng)格稱為“對象關(guān)聯(lián)”。
JavaScript 中就是沒有類似“類”的抽象機(jī)制逼庞。
對象關(guān)聯(lián)風(fēng)格的代碼還有一些不同之處蛇更。
(1)在 [[Prototype]] 委托中最好把狀態(tài)保存在委托者(XYZ、ABC)而不是委托目標(biāo)(Task)上赛糟。
(2)在類設(shè)計模式中派任,我們故意讓父類(Task)和子類(XYZ)中都有 outputTask 方法,這樣就可以利用重寫(多態(tài))的優(yōu)勢璧南。在委托行為中則恰好相反:我們會盡量避免在[[Prototype]] 鏈的不同級別中使用相同的命名掌逛。
(3)setID(..) 方法,由于調(diào)用位置觸發(fā)了 this 的隱式綁定規(guī)則司倚,因此雖然 setID(..) 方法在 Task 中豆混,運(yùn)行時 this 仍然會綁定到 XYZ,這正是我們想要的动知。

  • 委托行為意味著某些對象(XYZ)在找不到屬性或者方法引用時會把這個請求委托給另一個對象(Task)皿伺。
  • 對象并不是按照父類到子類的關(guān)系垂直組織的,而是通過任意方向的委托關(guān)聯(lián)并排組織的盒粮。
  • 在 API 接口的設(shè)計中心傀,委托最好在內(nèi)部實(shí)現(xiàn),不要直接暴露出去拆讯。

你無法在兩個或兩個以上互相(雙向)委托的對象之間創(chuàng)建循環(huán)委托脂男。
互相委托理論上是可以正常工作的,在某些情況下這是非常有用的种呐。之所以要禁止互相委托宰翅,是因?yàn)橐娴拈_發(fā)者們發(fā)現(xiàn)在設(shè)置時檢查(并禁止!)一次無限循環(huán)引用要更加高效爽室,否則每次從對象中查找屬性時都需要進(jìn)行檢查汁讼。

通常來說,JavaScript 規(guī)范并不會控制瀏覽器中開發(fā)者工具對于特定值或者結(jié)構(gòu)的表示方式,瀏覽器和引擎可以自己選擇合適的方式來進(jìn)行解析嘿架,因此瀏覽器和工具的解析結(jié)果并不一定相同瓶珊。

比較思維模型

典型的(“原型”)面向?qū)ο箫L(fēng)格:

function Foo(who) {
  this.me = who;
}
Foo.prototype.identify = function() {
  return "I am " + this.me;
};
function Bar(who) {
  Foo.call( this, who );
}
Bar.prototype = Object.create( Foo.prototype );
Bar.prototype.speak = function() {
  alert( "Hello, " + this.identify() + "." );
};
var b1 = new Bar( "b1" );
var b2 = new Bar( "b2" );
b1.speak();
b2.speak();

對象關(guān)聯(lián)風(fēng)格:

Foo = {
  init: function(who) {
    this.me = who;
  },
  identify: function() {
    return "I am " + this.me;
  }
};
Bar = Object.create( Foo );
Bar.speak = function() {
  alert( "Hello, " + this.identify() + "." );
};
var b1 = Object.create( Bar );
b1.init( "b1" );
var b2 = Object.create( Bar );
b2.init( "b2" );
b1.speak();
b2.speak();

同樣利用 [[Prototype]] 把 b1 委托給 Bar 并把 Bar 委托給 Foo,和上一段代碼一模一樣耸彪。我們?nèi)匀粚?shí)現(xiàn)了三個對象之間的關(guān)聯(lián)伞芹。但是非常重要的一點(diǎn)是,這段代碼簡潔了許多蝉娜,我們只是把對象關(guān)聯(lián)起來唱较,并不需要那些既復(fù)雜又令人困惑的模仿類的行為(構(gòu)造函數(shù)、原型以及 new)召川。

JavaScript 機(jī)制有很強(qiáng)的內(nèi)部連貫性南缓。JavaScript 中的函數(shù)之所以可以訪問 call(..)、apply(..) 和 bind(..)荧呐,就是因?yàn)楹瘮?shù)本身是對象汉形。
對象關(guān)聯(lián)風(fēng)格的代碼顯然更加簡潔,因?yàn)檫@種代碼只關(guān)注一件事:對象之間的關(guān)聯(lián)關(guān)系倍阐。

類與對象

class 仍然是通過 [[Prototype]]機(jī)制實(shí)現(xiàn)的获雕。

更好的語法

ES6 的 class 語法可以簡潔地定義類方法。

class Foo {
  methodName() { /* .. */ }
}

在 ES6 中 我 們 可 以 在 任 意 對 象 的 字 面 形 式 中 使 用 簡 潔 方 法 聲 明收捣。

var LoginController = {
  errors: [],
  getUser() { // 媽媽再也不用擔(dān)心代碼里有 function 了届案!
    // ...
  },
  getPassword() {
  // ...
  }
  // ...
};

唯一的區(qū)別是對象的字面形式仍然需要使用“,”來分隔元素,而 class 語法不需要罢艾。

// 使用更好的對象字面形式語法和簡潔方法
var AuthController = {
  errors: [],
  checkAuth() {
    // ...
  },
  server(url,data) {
    // ...
  }
  // ...
};
// 現(xiàn)在把 AuthController 關(guān)聯(lián)到 LoginController
Object.setPrototypeOf( AuthController, LoginController );

簡潔方法有一個非常小但是非常重要的缺點(diǎn)楣颠。

var Foo = {
  bar() { /*..*/ },
  baz: function baz() { /*..*/ }
};
// 去掉語法糖之后的代碼如下所示:
var Foo = {
  bar: function() { /*..*/ },
  baz: function baz() { /*..*/ }
};

由 于 函 數(shù) 對 象 本 身 沒 有 名 稱 標(biāo) 識 符, 所 以 bar() 的 縮 寫 形 式
(function()..)實(shí)際上會變成一個匿名函數(shù)表達(dá)式并賦值給 bar 屬性咐蚯。相比之下童漩,具名函數(shù)表達(dá)式(function baz()..)會額外給 .baz 屬性附加一個詞法名稱標(biāo)識符 baz。
匿名函數(shù)沒有 name 標(biāo)識符春锋,這會導(dǎo)致:

  1. 調(diào)試棧更難追蹤矫膨;
  2. 自我引用(遞歸、事件(解除)綁定期奔,等等)更難侧馅;
  3. 代碼(稍微)更難理解。

簡潔方法沒有第 1 和第 3 個缺點(diǎn)呐萌。
使用簡潔方法時一定要小心這一點(diǎn)馁痴。如果你需要自我引用的話,那最好使用傳統(tǒng)的具名函數(shù)表達(dá)式來定義對應(yīng)的函數(shù)肺孤,不要使用簡潔方法罗晕。

內(nèi)省

內(nèi)省就是檢查實(shí)例的類型济欢。類實(shí)例的自省主要目的是通過創(chuàng)建方式來判斷對象的結(jié)構(gòu)和功能。

function Foo() {
  // ...
}
Foo.prototype.something = function(){
  // ...
}
var a1 = new Foo();
// 之后
if (a1 instanceof Foo) {
  a1.something();
}

因 為 Foo.prototype( 不 是 Foo P≡ā) 在 a1 的 [[Prototype]] 鏈 上法褥, 所 以instanceof 操作(會令人困惑地)告訴我們 a1 是 Foo“類”的一個實(shí)例。從語法角度來說酬屉,instanceof 似乎是檢查 a1 和 Foo 的關(guān)系半等,但是實(shí)際上它想說的是 a1 和 Foo.prototype(引用的對象)是互相關(guān)聯(lián)的。
instanceof 語法會產(chǎn)生語義困惑而且非常不直觀梆惯。

function Foo() { /* .. */ }
Foo.prototype...
function Bar() { /* .. */ }
Bar.prototype = Object.create( Foo.prototype );
var b1 = new Bar( "b1" );
// ---------------使用 instanceof 和 .prototype 語義來檢查本例中實(shí)體的關(guān)系
// 讓 Foo 和 Bar 互相關(guān)聯(lián)
Bar.prototype instanceof Foo; // true
Object.getPrototypeOf( Bar.prototype )
=== Foo.prototype; // true
Foo.prototype.isPrototypeOf( Bar.prototype ); // true
// 讓 b1 關(guān)聯(lián)到 Foo 和 Bar
b1 instanceof Foo; // true
b1 instanceof Bar; // true
Object.getPrototypeOf( b1 ) === Bar.prototype; // true
Foo.prototype.isPrototypeOf( b1 ); // true
Bar.prototype.isPrototypeOf( b1 ); // true

你最直觀的想法可能是使用 Bar instanceof Foo酱鸭,但是在 JavaScript 中這是行不通的吗垮,你必須使用 Bar.prototype instanceof Foo垛吗。
還有一種常見但是可能更加脆弱的內(nèi)省模式,鴨子類型”烁登。

if (a1.something) {
  a1.something();
}
  • 我們并沒有檢查 a1 和委托 something() 函數(shù)的對象之間的關(guān)系怯屉,而是假設(shè)如果 a1 通過了測試 a1.something 的話,那 a1 就一定能調(diào)用.something()(無論這個方法存在于 a1 自身還是委托到其他對象)饵沧。
  • ES6 的 Promise 就是典型的“鴨子類型”锨络。如果對象有 then() 方法,ES6 的 Promise 就會認(rèn)為這個對象是“可持續(xù)”(thenable)的狼牺,因此會期望它具有 Promise 的所有標(biāo)準(zhǔn)行為羡儿。

對象關(guān)聯(lián)風(fēng)格代碼,其內(nèi)省更加簡潔是钥。使用對象關(guān)聯(lián)時掠归,所有的對象都是通過 [[Prototype]] 委托互相關(guān)聯(lián),下面是內(nèi)省的方法:

// 讓 Foo 和 Bar 互相關(guān)聯(lián)
Foo.isPrototypeOf( Bar ); // true
Object.getPrototypeOf( Bar ) === Foo; // true
// 讓 b1 關(guān)聯(lián)到 Foo 和 Bar
Foo.isPrototypeOf( b1 ); // true
Bar.isPrototypeOf( b1 ); // true
Object.getPrototypeOf( b1 ) === Bar; // true

總結(jié)

行為委托認(rèn)為對象之間是兄弟關(guān)系悄泥,互相委托虏冻,而不是父類和子類的關(guān)系。JavaScript 的[[Prototype]] 機(jī)制本質(zhì)上就是行為委托機(jī)制弹囚。

原文:
《你不知道的javascript上卷》摘要(上)
《你不知道的javascript上卷》摘要(下)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末厨相,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子鸥鹉,更是在濱河造成了極大的恐慌蛮穿,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毁渗,死亡現(xiàn)場離奇詭異绪撵,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)祝蝠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進(jìn)店門音诈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來幻碱,“玉大人,你說我怎么就攤上這事细溅∪彀” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵喇聊,是天一觀的道長恍风。 經(jīng)常有香客問我,道長誓篱,這世上最難降的妖魔是什么朋贬? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮窜骄,結(jié)果婚禮上锦募,老公的妹妹穿的比我還像新娘。我一直安慰自己邻遏,他們只是感情好糠亩,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著准验,像睡著了一般赎线。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上糊饱,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天垂寥,我揣著相機(jī)與錄音,去河邊找鬼另锋。 笑死滞项,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的砰蠢。 我是一名探鬼主播蓖扑,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼台舱!你這毒婦竟也來了律杠?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤竞惋,失蹤者是張志新(化名)和其女友劉穎柜去,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拆宛,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡嗓奢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了浑厚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片股耽。...
    茶點(diǎn)故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡根盒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出物蝙,到底是詐尸還是另有隱情炎滞,我是刑警寧澤,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布诬乞,位于F島的核電站瘤礁,受9級特大地震影響势誊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜判哥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一葱淳、第九天 我趴在偏房一處隱蔽的房頂上張望宅倒。 院中可真熱鬧慎璧,春花似錦偷溺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽证芭。三九已至瞳浦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間废士,已是汗流浹背叫潦。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留官硝,地道東北人矗蕊。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像氢架,于是被迫代替她去往敵國和親傻咖。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容