《你不知道的JavaScript(上卷)》

前言

本文作為對(duì)本書(shū)的一些知識(shí)點(diǎn)的收集

正文

1. LHSRHS

當(dāng)變量出現(xiàn)在賦值操作的左側(cè)時(shí)進(jìn)行 LHS 查詢酥泛,出現(xiàn)在右側(cè)時(shí)進(jìn)行 RHS 查詢;講得再準(zhǔn)確一點(diǎn)芋哭,RHS 查詢與簡(jiǎn)單地查找某個(gè)變量的值別無(wú)二致袁辈,而 LHS 查詢則是試圖找到變量的容器本身挨措;從這個(gè)角度說(shuō)汗洒,RHS 并不是真正意義上的 “賦值操作的右側(cè)” 议纯,更準(zhǔn)確地說(shuō)是 “非左側(cè)”;可以將 RHS 理解成 retrieve his source value(取到它的源值)溢谤,這意味著 “得到某某的值”

2. ReferenceErrorTypeError

ReferenceError 同作用域判別失敗相關(guān)瞻凤,而 TypeError 則代表作用域判別成功了,但是對(duì)
結(jié)果的操作是非法或不合理的世杀。

3. 不成功的 RHS 引用會(huì)導(dǎo)致拋出 ReferenceError 異常阀参。不成功的 LHS 引用會(huì)導(dǎo)致自動(dòng)隱式地創(chuàng)建一個(gè)全局變量(非嚴(yán)格模式下),該變量使用 LHS 引用的目標(biāo)作為標(biāo)識(shí)符瞻坝,或者拋

ReferenceError 異常(嚴(yán)格模式下)结笨。

4. 小測(cè)驗(yàn)答案

function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );

// 1. 找出所有的 LHS 查詢(這里有 3 處!)
c = ..;湿镀、a = 2(隱式變量分配)、b = ..
// 2. 找出所有的 RHS 查詢(這里有 4 處7ズ丁)
foo(2..勉痴、= a;、a ..树肃、.. b

5. 個(gè)不推薦使用 eval(..)with 的原因

會(huì)被嚴(yán)格模式所影響(限制)蒸矛。with 被完全禁止,而在保留核心功能的前提下,間接或非安全地使用 eval(..) 也被禁止了雏掠。

JavaScript 引擎會(huì)在編譯階段進(jìn)行數(shù)項(xiàng)的性能優(yōu)化斩祭。其中有些優(yōu)化依賴于能夠根據(jù)代碼的詞法進(jìn)行靜態(tài)分析,并預(yù)先確定所有變量和函數(shù)的定義位置乡话,才能在執(zhí)行過(guò)程中快速找到標(biāo)識(shí)符摧玫。
但如果引擎在代碼中發(fā)現(xiàn)了 eval(..) 或 with,它只能簡(jiǎn)單地假設(shè)關(guān)于標(biāo)識(shí)符位置的判斷都是無(wú)效的绑青,因?yàn)闊o(wú)法在詞法分析階段明確知道 eval(..) 會(huì)接收到什么代碼诬像,這些代碼會(huì)如何對(duì)作用域進(jìn)行修改,也無(wú)法知道傳遞給 with 用來(lái)創(chuàng)建新詞法作用域的對(duì)象的內(nèi)容到底是什么闸婴。
最悲觀的情況是如果出現(xiàn)了 eval(..) 或 with坏挠,所有的優(yōu)化可能都是無(wú)意義的,因此最簡(jiǎn)單的做法就是完全不做任何優(yōu)化邪乍。
如果代碼中大量使用 eval(..) 或 with降狠,那么運(yùn)行起來(lái)一定會(huì)變得非常慢。無(wú)論引擎多聰明庇楞,試圖將這些悲觀情況的副作用限制在最小范圍內(nèi)榜配,也無(wú)法避免如果沒(méi)有這些優(yōu)化,代碼會(huì)運(yùn)行得更慢這個(gè)事實(shí)姐刁。

6. eval(..) 和 with 小結(jié)

JavaScript 中有兩個(gè)機(jī)制可以“欺騙”詞法作用域:eval(..) 和 with芥牌。前者可以對(duì)一段包含一個(gè)或多個(gè)聲明的“代碼”字符串進(jìn)行演算,并借此來(lái)修改已經(jīng)存在的詞法作用域(在運(yùn)行時(shí))聂使。后者本質(zhì)上是通過(guò)將一個(gè)對(duì)象的引用當(dāng)作作用域來(lái)處理壁拉,將對(duì)象的屬性當(dāng)作作用域中的標(biāo)識(shí)符來(lái)處理,從而創(chuàng)建了一個(gè)新的詞法作用域(同樣是在運(yùn)行時(shí))柏靶。
這兩個(gè)機(jī)制的副作用是引擎無(wú)法在編譯時(shí)對(duì)作用域查找進(jìn)行優(yōu)化弃理,因?yàn)橐嬷荒苤?jǐn)慎地認(rèn)為這樣的優(yōu)化是無(wú)效的。使用這其中任何一個(gè)機(jī)制都將導(dǎo)致代碼運(yùn)行變慢屎蜓。不要使用它們痘昌。

7. 函數(shù)聲明和函數(shù)表達(dá)式

區(qū)分函數(shù)聲明和表達(dá)式最簡(jiǎn)單的方法是看 function 關(guān)鍵字出現(xiàn)在聲明中的位
置(不僅僅是一行代碼,而是整個(gè)聲明中的位置)炬转。如果 function 是聲明中
的第一個(gè)詞辆苔,那么就是一個(gè)函數(shù)聲明,否則就是一個(gè)函數(shù)表達(dá)式扼劈。

8. 在 JavaScript 中被稱為模塊的模式

function CoolModule() {
  var something = "cool";
  var another = [1, 2, 3];
  function doSomething() {
    console.log( something );
  }
  function doAnother() {
  console.log( another.join( " ! " ) );
  }
  return {
    doSomething: doSomething,
    doAnother: doAnother
  };
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

9. 當(dāng)只需要一個(gè)實(shí)例時(shí)驻啤,可以對(duì)上面這個(gè)模式進(jìn)行簡(jiǎn)單的改進(jìn)來(lái)實(shí)現(xiàn)單例模式:

var foo = (function CoolModule() {
  var something = "cool";
  var another = [1, 2, 3];
  function doSomething() {
    console.log( something );
  }
  function doAnother() {
    console.log( another.join( " ! " ) );
  }
  return {
    doSomething: doSomething,
    doAnother: doAnother
  };
})();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

10. 現(xiàn)代的模塊機(jī)制

大多數(shù)模塊依賴加載器 / 管理器本質(zhì)上都是將這種模塊定義封裝進(jìn)一個(gè)友好的 API。這里
并不會(huì)研究某個(gè)具體的庫(kù)荐吵,為了宏觀了解我會(huì)簡(jiǎn)單地介紹一些核心概念:

var MyModules = (function Manager() {
  var modules = {};
  function define(name, deps, impl) {
    for (var i=0; i<deps.length; i++) {
      deps[i] = modules[deps[i]];
    }
    modules[name] = impl.apply( impl, deps );
  }
  function get(name) {
    return modules[name];
  }
  return {
    define: define,
    get: get
  };
})();

這段代碼的核心是 modules[name] = impl.apply(impl, deps)骑冗。為了模塊的定義引入了包裝
函數(shù)(可以傳入任何依賴)赊瞬,并且將返回值,也就是模塊的 API贼涩,儲(chǔ)存在一個(gè)根據(jù)名字來(lái)管理的模塊列表中巧涧。

下面展示了如何使用它來(lái)定義模塊:

MyModules.define( "bar", [], function() {
  function hello(who) {
    return "Let me introduce: " + who;
  }
  return {
    hello: hello
  };
} );
MyModules.define( "foo", ["bar"], function(bar) {
  var hungry = "hippo";
  function awesome() {
    console.log( bar.hello( hungry ).toUpperCase() );
  }
  return {
    awesome: awesome
  };
} );
var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );
console.log(bar.hello( "hippo" )); // Let me introduce: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO

"foo" 和 "bar" 模塊都是通過(guò)一個(gè)返回公共 API 的函數(shù)來(lái)定義的。"foo" 甚至接受 "bar" 的
示例作為依賴參數(shù)遥倦,并能相應(yīng)地使用它

它們符合前面列出的模塊模式的兩個(gè)特點(diǎn):為函數(shù)定義引入包裝函數(shù)谤绳,并保證它的返回值和模塊的 API 保持一致。

11. 使用 new 來(lái)調(diào)用函數(shù)谊迄,或者說(shuō)發(fā)生構(gòu)造函數(shù)調(diào)用的原理

  • 舉例來(lái)說(shuō)闷供,思考一下 Number(..) 作為構(gòu)造函數(shù)時(shí)的行為,ES5.1 中這樣描述它:

15.7.2 Number 構(gòu)造函數(shù)
當(dāng) Number 在 new 表達(dá)式中被調(diào)用時(shí)统诺,它是一個(gè)構(gòu)造函數(shù):它會(huì)初始化新創(chuàng)建的對(duì)象歪脏。

  • 所以,包括內(nèi)置對(duì)象函數(shù)(比如 Number(..)粮呢,詳情請(qǐng)查看第 3 章)在內(nèi)的所有函數(shù)都可以用 new 來(lái)調(diào)用婿失,這種函數(shù)調(diào)用被稱為構(gòu)造函數(shù)調(diào)用
    這里有一個(gè)重要但是非常細(xì)微的區(qū)別:實(shí)際上并不存在所謂的“構(gòu)造函數(shù)”啄寡,只有對(duì)于函數(shù)的“構(gòu)造調(diào)用”豪硅。
  • 使用 new 來(lái)調(diào)用函數(shù),或者說(shuō)發(fā)生構(gòu)造函數(shù)調(diào)用時(shí)挺物,會(huì)自動(dòng)執(zhí)行下面的操作懒浮。
      1. 創(chuàng)建(或者說(shuō)構(gòu)造)一個(gè)全新的對(duì)象。
      1. 這個(gè)新對(duì)象會(huì)被執(zhí)行 [[ 原型 ]] 連接识藤。
      1. 這個(gè)新對(duì)象會(huì)綁定到函數(shù)調(diào)用的 this砚著。
      1. 如果函數(shù)沒(méi)有返回其他對(duì)象,那么 new 表達(dá)式中的函數(shù)調(diào)用會(huì)自動(dòng)返回這個(gè)新對(duì)象痴昧。

12. this 綁定的優(yōu)先級(jí)

  • 根據(jù)優(yōu)先級(jí)來(lái)判斷函數(shù)在某個(gè)調(diào)用位置應(yīng)用的是哪條規(guī)則稽穆。可以按照下面的順序來(lái)進(jìn)行判斷:
      1. 函數(shù)是否在 new 中調(diào)用(new 綁定)赶撰?如果是的話 this 綁定的是新創(chuàng)建的對(duì)象舌镶。
    var bar = new foo()
    
      1. 函數(shù)是否通過(guò) call、apply(顯式綁定)或者硬綁定調(diào)用豪娜?如果是的話餐胀,this 綁定的是
        指定的對(duì)象。
    var bar = foo.call(obj2)
    
      1. 函數(shù)是否在某個(gè)上下文對(duì)象中調(diào)用(隱式綁定)瘤载?如果是的話骂澄,this 綁定的是那個(gè)上下文對(duì)象。
    var bar = obj1.foo()
    
      1. 如果都不是的話惕虑,使用默認(rèn)綁定坟冲。如果在嚴(yán)格模式下,就綁定到 undefined溃蔫,否則綁定到全局對(duì)象健提。
    var bar = foo()
    
  • 在某些場(chǎng)景下 this 的綁定行為會(huì)出乎意料
    • 如果你把 null 或者 undefined 作為 this 的綁定對(duì)象傳入 callapply 或者 bind伟叛,這些值在調(diào)用時(shí)會(huì)被忽略私痹,實(shí)際應(yīng)用的是默認(rèn)綁定規(guī)則:
    function foo() {
      console.log( this.a );
    }
    var a = 2;
    foo.call( null ); // 2
    

13. 創(chuàng)建一個(gè)空對(duì)象最簡(jiǎn)單的方法

如果函數(shù)并不關(guān)心 this 的話,你
仍然需要傳入一個(gè)占位值统刮,使用這個(gè)方法比 null 更安全紊遵,它就是一個(gè)空的非委托的對(duì)象

// 因?yàn)?? 表示 “希望 this 是空”,這比 null 的含義更清楚侥蒙。不過(guò)你可以用任何喜歡的名字來(lái)命名 DMZ(demilitarized zone) 對(duì)象
var ? = Object.create(null)

Object.create(null){} 很 像暗膜, 但 是 并 不 會(huì) 創(chuàng) 建 Object.prototype 這個(gè)委托,所以它比 {}“更空”:

  • 代碼示例:
function foo(a,b) {
  console.log( "a:" + a + ", b:" + b );
}
// 我們的 DMZ 空對(duì)象
var ? = Object.create( null );
// 把數(shù)組展開(kāi)成參數(shù)
foo.apply( ?, [2, 3] ); // a:2, b:3
// 使用 bind(..) 進(jìn)行柯里化
var bar = foo.bind( ?, 2 );
bar( 3 ); // a:2, b:3

14. this 全面解析 小結(jié)

  • 如果要判斷一個(gè)運(yùn)行中函數(shù)的 this 綁定鞭衩,就需要找到這個(gè)函數(shù)的直接調(diào)用位置学搜。找到之后就可以順序應(yīng)用下面這四條規(guī)則來(lái)判斷 this 的綁定對(duì)象。

      1. new 調(diào)用论衍?綁定到新創(chuàng)建的對(duì)象瑞佩。
      1. call 或者 apply(或者 bind)調(diào)用?綁定到指定的對(duì)象坯台。
      1. 由上下文對(duì)象調(diào)用炬丸?綁定到那個(gè)上下文對(duì)象。
      1. 默認(rèn):在嚴(yán)格模式下綁定到 undefined蜒蕾,否則綁定到全局對(duì)象稠炬。

    一定要注意,有些調(diào)用可能在無(wú)意中使用默認(rèn)綁定規(guī)則滥搭。如果想“更安全”地忽略 this 綁定酸纲,你可以使用一個(gè) DMZ 對(duì)象,比如 ? = Object.create(null)瑟匆,以保護(hù)全局對(duì)象闽坡。

    ES6 中的箭頭函數(shù)并不會(huì)使用四條標(biāo)準(zhǔn)的綁定規(guī)則,而是根據(jù)當(dāng)前的詞法作用域來(lái)決定 this愁溜,具體來(lái)說(shuō)疾嗅,箭頭函數(shù)會(huì)繼承外層函數(shù)調(diào)用的 this 綁定(無(wú)論 this 綁定到什么)。這其實(shí)和 ES6 之前代碼中的 self = this 機(jī)制一樣冕象。

15. 類型

  • 對(duì)象是 JavaScript 的基礎(chǔ)代承。在 JavaScript 中一共有六種主要類型(術(shù)語(yǔ)是“語(yǔ)言類型”):
    • string
    • number
    • boolean
    • null
    • undefined
    • object

注意,簡(jiǎn)單基本類型(string渐扮、boolean论悴、number掖棉、nullundefined)本身并不是對(duì)象。

null 有時(shí)會(huì)被當(dāng)作一種對(duì)象類型膀估,但是這其實(shí)只是語(yǔ)言本身的一個(gè) bug幔亥,即對(duì) null 執(zhí)行 typeof null 時(shí)會(huì)返回字符串 "object"。實(shí)際上察纯,null 本身是基本類型帕棉。

原理是這樣的,不同的對(duì)象在底層都表示為二進(jìn)制饼记,在 JavaScript 中二進(jìn)制前三位都為 0 的話會(huì)被判斷為 object 類型香伴,null 的二進(jìn)制表示是全 0,自然前三位也是 0具则,所以執(zhí)行 typeof 時(shí)會(huì)返回“object”即纲。

有一種常見(jiàn)的錯(cuò)誤說(shuō)法是“JavaScript 中萬(wàn)物皆是對(duì)象”,這顯然是錯(cuò)誤的乡洼。

實(shí)際上崇裁,JavaScript 中有許多特殊的對(duì)象子類型,我們可以稱之為復(fù)雜基本類型束昵。

函數(shù)就是對(duì)象的一個(gè)子類型(從技術(shù)角度來(lái)說(shuō)就是“可調(diào)用的對(duì)象”)拔稳。JavaScript 中的函數(shù)是“一等公民”,因?yàn)樗鼈儽举|(zhì)上和普通的對(duì)象一樣(只是可以調(diào)用)锹雏,所以可以像操作其他對(duì)象一樣操作函數(shù)(比如當(dāng)作另一個(gè)函數(shù)的參數(shù))巴比。

數(shù)組也是對(duì)象的一種類型,具備一些額外的行為礁遵。數(shù)組中內(nèi)容的組織方式比一般的對(duì)象要稍微復(fù)雜一些轻绞。

16. 內(nèi)置對(duì)象

  • JavaScript 中還有一些對(duì)象子類型,通常被稱為內(nèi)置對(duì)象佣耐。有些內(nèi)置對(duì)象的名字看起來(lái)和簡(jiǎn)單基礎(chǔ)類型一樣政勃,不過(guò)實(shí)際上它們的關(guān)系更復(fù)雜。
    • String
    • Number
    • Boolean
    • Object
    • Function
    • Array
    • Date
    • RegExp
    • Error

JavaScript 中兼砖,這些內(nèi)置對(duì)象實(shí)際上只是一些內(nèi)置函數(shù)奸远。這些內(nèi)置函數(shù)可以當(dāng)作構(gòu)造函數(shù)(由 new 產(chǎn)生的函數(shù)調(diào)用)來(lái)使用,從而可以構(gòu)造一個(gè)對(duì)應(yīng)子類型的新對(duì)象讽挟。舉例來(lái)說(shuō):

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 對(duì)象
Object.prototype.toString.call( strObject ); // [object String]

從代碼中可以看到懒叛,strObject 是由 String 構(gòu)造函數(shù)創(chuàng)建的一個(gè)對(duì)象。

原始值 "I am a string" 并不是一個(gè)對(duì)象耽梅,它只是一個(gè)字面量薛窥,并且是一個(gè)不可變的值。

如果要在這個(gè)字面量上執(zhí)行一些操作眼姐,比如獲取長(zhǎng)度诅迷、訪問(wèn)其中某個(gè)字符等佩番,那需要將其轉(zhuǎn)換為 String 對(duì)象。

var strPrimitive = "I am a string";
console.log( strPrimitive.length ); // 13
console.log( strPrimitive.charAt( 3 ) ); // "m"

使用以上兩種方法竟贯,我們都可以直接在字符串字面量上訪問(wèn)屬性或者方法答捕,之所以可以這樣做,是因?yàn)橐孀詣?dòng)把字面量轉(zhuǎn)換成 String 對(duì)象屑那,所以可以訪問(wèn)屬性和方法。
同樣的事也會(huì)發(fā)生在數(shù)值字面量上艘款,如果使用類似 42.359.toFixed(2) 的方法持际,引擎會(huì)把對(duì)象 42 轉(zhuǎn)換成 new Number(42)。對(duì)于布爾字面量來(lái)說(shuō)也是如此哗咆。

nullundefined 沒(méi)有對(duì)應(yīng)的構(gòu)造形式蜘欲,它們只有文字形式。相反晌柬,Date 只有構(gòu)造姥份,沒(méi)有文字形式。

對(duì)于 Object年碘、Array澈歉、FunctionRegExp(正則表達(dá)式)來(lái)說(shuō),無(wú)論使用文字形式還是構(gòu)造形式屿衅,它們都是對(duì)象埃难,不是字面量。在某些情況下涤久,相比用文字形式創(chuàng)建對(duì)象涡尘,構(gòu)造形式可以提供一些額外選項(xiàng)。由于這兩種形式都可以創(chuàng)建對(duì)象响迂,所以我們首選更簡(jiǎn)單的文字形式考抄。建議只在需要那些額外選項(xiàng)時(shí)使用構(gòu)造形式。

Error 對(duì)象很少在代碼中顯式創(chuàng)建蔗彤,一般是在拋出異常時(shí)被自動(dòng)創(chuàng)建川梅。也可以使用 new Error(..) 這種構(gòu)造形式來(lái)創(chuàng)建,不過(guò)一般來(lái)說(shuō)用不著幕与。

17. 內(nèi)容

  • 需要強(qiáng)調(diào)的一點(diǎn)是挑势,當(dāng)我們說(shuō)“內(nèi)容”時(shí),似乎在暗示這些值實(shí)際上被存儲(chǔ)在對(duì)象內(nèi)部啦鸣,但是這只是它的表現(xiàn)形式潮饱。在引擎內(nèi)部,這些值的存儲(chǔ)方式是多種多樣的诫给,一般并不會(huì)存在對(duì)象容器內(nèi)部香拉。存儲(chǔ)在對(duì)象容器內(nèi)部的是這些屬性的名稱啦扬,它們就像指針(從技術(shù)角度來(lái)說(shuō)就是引用)一樣,指向這些值真正的存儲(chǔ)位置凫碌。
var myObject = {
a: 2
};
myObject.a; // 2
myObject["a"]; // 2

如果要訪問(wèn) myObject 中 a 位置上的值扑毡,我們需要使用 . 操作符或者 [] 操作符。.a 語(yǔ)法通常被稱為“屬性訪問(wèn)”盛险,["a"] 語(yǔ)法通常被稱為“鍵訪問(wèn)”瞄摊。實(shí)際上它們?cè)L問(wèn)的是同一個(gè)位置,并且會(huì)返回相同的值 2苦掘,所以這兩個(gè)術(shù)語(yǔ)是可以互換的换帜。

這兩種語(yǔ)法的主要區(qū)別在于 . 操作符要求屬性名滿足標(biāo)識(shí)符的命名規(guī)范,而 [".."] 語(yǔ)法可以接受任意 UTF-8/Unicode 字符串作為屬性名鹤啡。舉例來(lái)說(shuō)惯驼,如果要引用名稱為 "SuperFun!"的屬性,那就必須使用 ["Super-Fun!"] 語(yǔ)法訪問(wèn)递瑰,因?yàn)?Super-Fun! 并不是一個(gè)有效的標(biāo)識(shí)符屬性名祟牲。

  • 在對(duì)象中,屬性名永遠(yuǎn)都是字符串抖部。如果你使用 string(字面量)以外的其他值作為屬性名说贝,那它首先會(huì)被轉(zhuǎn)換為一個(gè)字符串。即使是數(shù)字也不例外您朽,雖然在數(shù)組下標(biāo)中使用的的確是數(shù)字狂丝,但是在對(duì)象屬性名中數(shù)字會(huì)被轉(zhuǎn)換成字符串,所以當(dāng)心不要搞混對(duì)象和數(shù)組中數(shù)字的用法:
var myObject = { };
myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";
myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"

18. 可計(jì)算屬性名

  • ES6 增加了可計(jì)算屬性名哗总,可以在文字形式中使用 [] 包裹一個(gè)表達(dá)式來(lái)當(dāng)作屬性名:
var prefix = "foo";
var myObject = {
  [prefix + "bar"]:"hello",
  [prefix + "baz"]: "world"
};
myObject["foobar"]; // hello
myObject["foobaz"]; // world

19. 復(fù)制對(duì)象

  • 對(duì)于 JSON 安全(也就是說(shuō)可以被序列化為一個(gè) JSON 字符串并且可以根據(jù)這個(gè)字符串解析出一個(gè)結(jié)構(gòu)和值完全一樣的對(duì)象)的對(duì)象來(lái)說(shuō)几颜,有一種巧妙的復(fù)制方法:
var newObj = JSON.parse( JSON.stringify( someObj ) );

當(dāng)然,這種方法需要保證對(duì)象是 JSON 安全的讯屈,所以只適用于部分情況蛋哭。

  • ES6 定義了 Object.assign(..) 方法來(lái)實(shí)現(xiàn)淺復(fù)制。

Object.assign(..) 方法的第一個(gè)參數(shù)是目標(biāo)對(duì)象涮母,之后還可以跟一個(gè)或多個(gè)源對(duì)象谆趾。它會(huì)遍歷一個(gè)或多個(gè)源對(duì)象的所有可枚舉(enumerable,參見(jiàn)下面的代碼)的自有鍵(owned key)并把它們復(fù)制(使用 = 操作符賦值)到目標(biāo)對(duì)象叛本,最后返回目標(biāo)對(duì)象沪蓬,就像這樣:

var newObj = Object.assign( {}, myObject );
newObj.a; // 2
newObj.b === anotherObject; // true
newObj.c === anotherArray; // true
newObj.d === anotherFunction; // true

由于 Object.assign(..) 就是使用 = 操作符來(lái)賦值,所
以源對(duì)象屬性的一些特性(比如 writable)不會(huì)被復(fù)制到目標(biāo)對(duì)象来候。

20. 屬性描述符

  • 在 ES5 之前跷叉,JavaScript 語(yǔ)言本身并沒(méi)有提供可以直接檢測(cè)屬性特性的方法,比如判斷屬性是否是只讀。但是從 ES5 開(kāi)始云挟,所有的屬性都具備了屬性描述符梆砸。
var myObject = {
  a:2
};
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }

這個(gè)普通的對(duì)象屬性對(duì)應(yīng)的屬性描述符(也被稱為“數(shù)據(jù)描述符”,因?yàn)樗槐4嬉粋€(gè)數(shù)據(jù)值)可不僅僅只是一個(gè) 2园欣。它還包含另外三個(gè)特性:writable(可寫)帖世、enumerable(可枚舉)和 configurable(可配置)。

  • 可以使用 Object.defineProperty(..) 來(lái)添加一個(gè)新屬性或者修改一個(gè)已有屬性(如果它是 configurable)并對(duì)特性進(jìn)行設(shè)置沸枯。
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true
} );
myObject.a; // 2

我們使用 defineProperty(..)myObject 添加了一個(gè)普通的屬性并顯式指定了一些特性日矫。然而,一般來(lái)說(shuō)你不會(huì)使用這種方式绑榴,除非你想修改屬性描述符搬男。

  • writable 決定是否可以修改屬性的值。
var myObject = {};
Object.defineProperty( myObject, "a", {
  value: 2,
  writable: false, // 不可寫彭沼!
  configurable: true,
  enumerable: true
} );
myObject.a = 3;
myObject.a; // 2

如你所見(jiàn),我們對(duì)于屬性值的修改靜默失敱赴!(silently failed)了姓惑。如果在嚴(yán)格模式下,這種方法會(huì)出錯(cuò):

"use strict";
var myObject = {};
Object.defineProperty( myObject, "a", {
  value: 2,
  writable: false, // 不可寫按脚!
  configurable: true,
  enumerable: true
} );
myObject.a = 3; // TypeError

TypeError 錯(cuò)誤表示我們無(wú)法修改一個(gè)不可寫的屬性于毙。

  • configurable 只要屬性是可配置的,就可以使用 defineProperty(..) 方法來(lái)修改屬性描述符:
var myObject = {
  a:2
};
myObject.a = 3;
myObject.a; // 3
Object.defineProperty( myObject, "a", {
  value: 4,
  writable: true,
  configurable: false, // 不可配置辅搬!
  enumerable: true
} );
myObject.a; // 4
myObject.a = 5;
myObject.a; // 5
Object.defineProperty( myObject, "a", {
  value: 6,
  writable: true,
  configurable: true,
  enumerable: true
} ); // TypeError

最后一個(gè) defineProperty(..) 會(huì)產(chǎn)生一個(gè) TypeError 錯(cuò)誤唯沮,不管是不是處于嚴(yán)格模式,嘗試修改一個(gè)不可配置的屬性描述符都會(huì)出錯(cuò)堪遂。注意:如你所見(jiàn)介蛉,把 configurable 修改成 false 是單向操作,無(wú)法撤銷溶褪!
要注意有一個(gè)小小的例外:即便屬性是 configurable:false币旧,我們還是可以把 writable 的狀態(tài)由 true 改為 false,但是無(wú)法由 false 改為 true猿妈。

21. 不變性

  • 對(duì)象常量(不可修改吹菱、重定義或者刪除)
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
  value: 42,
  writable: false,
  configurable: false
} );
  • 禁止擴(kuò)展(禁 止 一 個(gè) 對(duì) 象 添 加 新 屬 性 并 且 保 留 已 有 屬 性)
var myObject = {
a:2
};
Object.preventExtensions( myObject );
myObject.b = 3;
myObject.b; // undefined

在非嚴(yán)格模式下,創(chuàng)建屬性 b 會(huì)靜默失敗彭则。在嚴(yán)格模式下鳍刷,將會(huì)拋出 TypeError 錯(cuò)誤。

  • 密封

Object.seal(..) 會(huì)創(chuàng)建一個(gè)“密封”的對(duì)象俯抖,這個(gè)方法實(shí)際上會(huì)在一個(gè)現(xiàn)有對(duì)象上調(diào)用 Object.preventExtensions(..) 并把所有現(xiàn)有屬性標(biāo)記為 configurable:false输瓜。所以,密封之后不僅不能添加新屬性,也不能重新配置或者刪除任何現(xiàn)有屬性(雖然可以修改屬性的值)

  • 凍結(jié)

Object.freeze(..) 會(huì)創(chuàng)建一個(gè)凍結(jié)對(duì)象前痘,這個(gè)方法實(shí)際上會(huì)在一個(gè)現(xiàn)有對(duì)象上調(diào)用 Object.seal(..) 并把所有“數(shù)據(jù)訪問(wèn)”屬性標(biāo)記為 writable:false凛捏,這樣就無(wú)法修改它們的值。

這個(gè)方法是你可以應(yīng)用在對(duì)象上的級(jí)別最高的不可變性芹缔,它會(huì)禁止對(duì)于對(duì)象本身及其任意直接屬性的修改(不過(guò)就像我們之前說(shuō)過(guò)的坯癣,這個(gè)對(duì)象引用的其他對(duì)象是不受影響的)。

你可以“深度凍結(jié)”一個(gè)對(duì)象最欠,具體方法為示罗,首先在這個(gè)對(duì)象上調(diào)用 Object.freeze(..),然后遍歷它引用的所有對(duì)象并在這些對(duì)象上調(diào)用 Object.freeze(..)芝硬。但是一定要小心蚜点,因
為這樣做有可能會(huì)在無(wú)意中凍結(jié)其他(共享)對(duì)象。

22. 存在性

前面我們介紹過(guò)拌阴,如 myObject.a 的屬性訪問(wèn)返回值可能是 undefined绍绘,但是這個(gè)值有可能是屬性中存儲(chǔ)的 undefined,也可能是因?yàn)閷傩圆淮嬖谒苑祷?undefined迟赃。那么如何區(qū)分這兩種情況呢陪拘?

  • 可以在不訪問(wèn)屬性值的情況下判斷對(duì)象中是否存在這個(gè)屬性:
var myObject = {
a:2
};
("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "b" ); // false

in 操作符會(huì)檢查屬性是否在對(duì)象及其 [[Prototype]] 原型鏈中相比之下,hasOwnProperty(..) 只會(huì)檢查屬性是否在 myObject 對(duì)象中纤壁,不會(huì)檢查 [[Prototype]] 鏈左刽。

所有的普通對(duì)象都可以通過(guò)對(duì)于 Object.prototype的 委 托 來(lái)訪問(wèn) hasOwnProperty(..),但是有的對(duì)象可能沒(méi)有連接到 Object.prototype(通過(guò) Object.create(null) 來(lái)創(chuàng)建——參見(jiàn)第 5 章)酌媒。在這種情況下欠痴,形如myObejct.hasOwnProperty(..)就會(huì)失敗。
這 時(shí) 可 以 使 用 一 種 更 加 強(qiáng) 硬 的 方 法 來(lái) 進(jìn) 行 判 斷:Object.prototype.hasOwnProperty.call(myObject,"a")秒咨,它借用基礎(chǔ)的 hasOwnProperty(..) 方法并把它顯式綁定到 myObject 上喇辽。

看起來(lái) in 操作符可以檢查容器內(nèi)是否有某個(gè)值,但是它實(shí)際上檢查的是某個(gè)屬性名是否存在拭荤。對(duì)于數(shù)組來(lái)說(shuō)這個(gè)區(qū)別非常重要茵臭,4 in [2, 4, 6] 的結(jié)果并不是你期待的 True,因?yàn)?[2, 4, 6] 這個(gè)數(shù)組中包含的屬性名是 0舅世、1旦委、2,沒(méi)有 4雏亚。

  • 枚舉(什么是“可枚舉性”)
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.b 確實(shí)存在并且有訪問(wèn)值,但是卻不會(huì)出現(xiàn)在 for..in 循環(huán)中(盡管可以通過(guò) in 操作符來(lái)判斷是否存在)罢低。原因是“可枚舉”就相當(dāng)于“可以出現(xiàn)在對(duì)象屬性的遍歷中”查辩。

在數(shù)組上應(yīng)用 for..in 循環(huán)有時(shí)會(huì)產(chǎn)生出人意料的結(jié)果胖笛,因?yàn)檫@種枚舉不僅會(huì)包含所有數(shù)值索引,還會(huì)包含所有可枚舉屬性宜岛。最好只在對(duì)象上應(yīng)用 for..in 循環(huán)长踊,如果要遍歷數(shù)組就使用傳統(tǒng)的 for 循環(huán)來(lái)遍歷數(shù)值索引。

  • 也可以通過(guò)另一種方式來(lái)區(qū)分屬性是否可枚舉:
var myObject = { };
Object.defineProperty(
  myObject,
  "a",
  // 讓 a 像普通屬性一樣可以枚舉
  { enumerable: true, value: 2 }
);
Object.defineProperty(
  myObject,
  "b",
  // 讓 b 不可枚舉
  { enumerable: false, value: 3 }
);
myObject.propertyIsEnumerable( "a" ); // true
myObject.propertyIsEnumerable( "b" ); // false
Object.keys( myObject ); // ["a"]
Object.getOwnPropertyNames( myObject ); // ["a", "b"]

propertyIsEnumerable(..) 會(huì)檢查給定的屬性名是否直接存在于對(duì)象中(而不是在原型鏈上)并且滿足 enumerable:true萍倡。
Object.keys(..) 會(huì)返回一個(gè)數(shù)組身弊,包含所有可枚舉屬性
Object.getOwnPropertyNames(..)會(huì)返回一個(gè)數(shù)組,包含所有屬性列敲,無(wú)論它們是否可枚舉阱佛。

inhasOwnProperty(..) 的區(qū)別在于是否查找 [[Prototype]] 鏈,然而戴而,Object.keys(..)Object.getOwnPropertyNames(..) 都只會(huì)查找對(duì)象直接包含的屬性凑术。

(目前)并沒(méi)有內(nèi)置的方法可以獲取 in 操作符使用的屬性列表(對(duì)象本身的屬性以及 [[Prototype]] 鏈中的所有屬性,參見(jiàn)第 5 章)所意。不過(guò)你可以遞歸遍歷某個(gè)對(duì)象的整條 [[Prototype]] 鏈并保存每一層中使用 Object.keys(..) 得到的屬性列表——只包含可枚舉屬性淮逊。

23. 遍歷

  • 對(duì)于數(shù)值索引的數(shù)組來(lái)說(shuō),可以使用標(biāo)準(zhǔn)的 for 循環(huán)來(lái)遍歷值:
var myArray = [1, 2, 3];
for (var i = 0; i < myArray.length; i++) {
  console.log( myArray[i] );
}
// 1 2 3

這實(shí)際上并不是在遍歷值扶踊,而是遍歷下標(biāo)來(lái)指向值壮莹,如 myArray[i]

  • ES5 中增加了一些數(shù)組的輔助迭代器姻檀,包括 forEach(..)every(..)some(..)涝滴。每種輔助迭代器都可以接受一個(gè)回調(diào)函數(shù)并把它應(yīng)用到數(shù)組的每個(gè)元素上绣版,唯一的區(qū)別就是它們對(duì)于回調(diào)函數(shù)返回值的處理方式不同。

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

    every(..)some(..) 中特殊的返回值和普通 for 循環(huán)中的 break 語(yǔ)句類似歼疮,它們會(huì)提前終止遍歷

    使用 for..in 遍歷對(duì)象是無(wú)法直接獲取屬性值的杂抽,因?yàn)樗鼘?shí)際上遍歷的是對(duì)象中的所有可枚舉屬性,你需要手動(dòng)獲取屬性值韩脏。

    遍歷數(shù)組下標(biāo)時(shí)采用的是數(shù)字順序(for 循環(huán)或者其他迭代器)缩麸,但是遍歷對(duì)象屬性時(shí)的順序是不確定的部服,在不同的 JavaScript 引擎中可能不一樣弄匕。因此奥额,在不同的環(huán)境中需要保證一致性時(shí)焚志,一定不要相信任何觀察到的順序先巴,它們是不可靠的丸升。

  • ES6 增加了一種用來(lái)遍歷數(shù)組的 for..of 循環(huán)語(yǔ)法(如果對(duì)象本身定義了迭代器的話也可以遍歷對(duì)象):

var myArray = [ 1, 2, 3 ];
for (var v of myArray) {
  console.log( v );
}
// 1
// 2
// 3

for..of 循環(huán)首先會(huì)向被訪問(wèn)對(duì)象請(qǐng)求一個(gè)迭代器對(duì)象粗仓,然后通過(guò)調(diào)用迭代器對(duì)象的 next() 方法來(lái)遍歷所有返回值譬挚。

數(shù)組有內(nèi)置的 @@iterator空民,因此 for..of 可以直接應(yīng)用在數(shù)組上刃唐。我們使用內(nèi)置的 @@iterator 來(lái)手動(dòng)遍歷數(shù)組,看看它是怎么工作的:

var myArray = [ 1, 2, 3 ];
var it = myArray[Symbol.iterator]();
it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { done:true }

我們使用 ES6 中的符號(hào) Symbol.iterator 來(lái)獲取對(duì)象的 @@iterator 內(nèi)部屬性。之前我們簡(jiǎn)單介紹過(guò)符(Symbol画饥,參見(jiàn) 3.3.1 節(jié))衔瓮,跟這里的原理是相同的。引用類似 iterator 的特殊屬性時(shí)要使用符號(hào)名抖甘,而不是符號(hào)包含的值热鞍。此外,雖然看起來(lái)很像一個(gè)對(duì)象单山,但是 @@iterator 本身并不是一個(gè)迭代器對(duì)象碍现,而是一個(gè)返回迭代器對(duì)象的函數(shù)——這點(diǎn)非常精妙并且非常重要。

如你所見(jiàn)米奸,調(diào)用迭代器的 next() 方法會(huì)返回形式為 { value: .. , done: .. } 的值昼接,value 是當(dāng)前的遍歷值,done 是一個(gè)布爾值悴晰,表示是否還有可以遍歷的值慢睡。
注意,和值“3”一起返回的是 done:false铡溪,乍一看好像很奇怪漂辐,你必須再調(diào)用一次 next() 才能得到 done:true,從而確定完成遍歷棕硫。這個(gè)機(jī)制和 ES6 中發(fā)生器函數(shù)的語(yǔ)義相關(guān)髓涯,不過(guò)已經(jīng)超出了我們的討論范圍。

和數(shù)組不同哈扮,普通的對(duì)象沒(méi)有內(nèi)置的 @@iterator纬纪,所以無(wú)法自動(dòng)完成 for..of 遍歷。之所以要這樣做滑肉,有許多非常復(fù)雜的原因包各,不過(guò)簡(jiǎn)單來(lái)說(shuō),這樣做是為了避免影響未來(lái)的對(duì)象類型靶庙。

當(dāng)然问畅,你可以給任何想遍歷的對(duì)象定義 @@iterator,舉例來(lái)說(shuō):

var myObject = {
  a: 2,
  b: 3
};
Object.defineProperty( myObject, Symbol.iterator, {
  enumerable: false,
  writable: false,
  configurable: true,
  value: function() {
    var o = this;
    var idx = 0;
    var ks = Object.keys( o );
    return {
      next: function() {
        return {
          value: o[ks[idx++]],
          done: (idx > ks.length)
        };
      }
    };
  }
} );
// 手動(dòng)遍歷 myObject
var it = myObject[Symbol.iterator]();
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { value:undefined, done:true }
// 用 for..of 遍歷 myObject
for (var v of myObject) {
  console.log( v );
}
// 2
// 3

我們使用 Object.defineProperty(..) 定義了我們自己的@@iterator(主要是為了讓它不可枚舉)六荒,不過(guò)注意护姆,我們把符號(hào)當(dāng)作可計(jì)算屬性名。此外掏击,也可以直接在定義對(duì)象時(shí)進(jìn)行聲明签则,比如 var myObject = { a:2, b:3, [Symbol.iterator]: function() { /* .. */ } }

  • 一個(gè)“無(wú)限”迭代器铐料,它永遠(yuǎn)不會(huì)“結(jié)束”并且總會(huì)返回一個(gè)新值(隨機(jī)數(shù))
var randoms = {
  [Symbol.iterator]: function() {
    return {
      next: function() {
        return { value: Math.random() };
      }
    };
  }
};
var randoms_pool = [];
for (var n of randoms) {
  randoms_pool.push( n );
  // 防止無(wú)限運(yùn)行渐裂!
  if (randoms_pool.length === 100) break;
}

24. 混合對(duì)象 “類” 小結(jié)

類是一種設(shè)計(jì)模式豺旬。許多語(yǔ)言提供了對(duì)于面向類軟件設(shè)計(jì)的原生語(yǔ)法。JavaScript 也有類似的語(yǔ)法柒凉,但是和其他語(yǔ)言中的類完全不同族阅。

類意味著復(fù)制。

傳統(tǒng)的類被實(shí)例化時(shí)膝捞,它的行為會(huì)被復(fù)制到實(shí)例中坦刀。類被繼承時(shí),行為也會(huì)被復(fù)制到子類中蔬咬。

多態(tài)(在繼承鏈的不同層次名稱相同但是功能不同的函數(shù))看起來(lái)似乎是從子類引用父類鲤遥,但是本質(zhì)上引用的其實(shí)是復(fù)制的結(jié)果。

JavaScript 并不會(huì)(像類那樣)自動(dòng)創(chuàng)建對(duì)象的副本林艘。

混入模式(無(wú)論顯式還是隱式)可以用來(lái)模擬類的復(fù)制行為盖奈,但是通常會(huì)產(chǎn)生丑陋并且脆弱的語(yǔ)法,比如顯式偽多態(tài)(OtherObj.methodName.call(this, ...))狐援,這會(huì)讓代碼更加難懂并且難以維護(hù)钢坦。
此外,顯式混入實(shí)際上無(wú)法完全模擬類的復(fù)制行為啥酱,因?yàn)閷?duì)象(和函數(shù)爹凹!別忘了函數(shù)也是對(duì)象)只能復(fù)制引用,無(wú)法復(fù)制被引用的對(duì)象或者函數(shù)本身镶殷。忽視這一點(diǎn)會(huì)導(dǎo)致許多問(wèn)題禾酱。

25. 原型 -- [[Prototype]]

使用 for..in 遍歷對(duì)象時(shí)原理和查找 [[Prototype]] 鏈類似,任何可以通過(guò)原型鏈訪問(wèn)到(并且是 enumerable绘趋,參見(jiàn)第 3 章)的屬性都會(huì)被枚舉宇植。使用 in 操作符來(lái)檢查屬性在對(duì)象中是否存在時(shí),同樣會(huì)查找對(duì)象的整條原型鏈(無(wú)論屬性是否可枚舉):

var anotherObject = {
  a:2
};
// 創(chuàng)建一個(gè)關(guān)聯(lián)到 anotherObject 的對(duì)象
var myObject = Object.create( anotherObject );
myObject.a; // 2

26. 原型 -- Object.prototype

所有普通的 [[Prototype]] 鏈最終都會(huì)指向內(nèi)置的 Object.prototype埋心。由于所有的“普通”(內(nèi)置,不是特定主機(jī)的擴(kuò)展)對(duì)象都“源于”(或者說(shuō)把 [[Prototype]] 鏈的頂端設(shè)置為)這個(gè) Object.prototype 對(duì)象忙上,所以它包含 JavaScript 中許多通用的功能拷呆。
有 些 功 能 你 應(yīng) 該 已 經(jīng) 很 熟 悉 了, 比 如 說(shuō) .toString().valueOf()疫粥, 第 3 章 還 介 紹過(guò) .hasOwnProperty(..)茬斧。稍后我們還會(huì)介紹 .isPrototypeOf(..),這個(gè)你可能不太熟悉梗逮。

27. 原型 - “類”函數(shù)

new Foo() 會(huì)生成一個(gè)新對(duì)象(我們稱之為 a)项秉,這個(gè)新對(duì)象的內(nèi)部鏈接 [[Prototype]] 關(guān)聯(lián)的是 Foo.prototype 對(duì)象。
最后我們得到了兩個(gè)對(duì)象慷彤,它們之間互相關(guān)聯(lián)娄蔼,就是這樣怖喻。我們并沒(méi)有初始化一個(gè)類,實(shí)際上我們并沒(méi)有從“類”中復(fù)制任何行為到一個(gè)對(duì)象中岁诉,只是讓兩個(gè)對(duì)象互相關(guān)聯(lián)锚沸。
實(shí)際上,絕大多數(shù) JavaScript 開(kāi)發(fā)者不知道的秘密是涕癣,new Foo() 這個(gè)函數(shù)調(diào)用實(shí)際上并沒(méi)有直接創(chuàng)建關(guān)聯(lián)哗蜈,這個(gè)關(guān)聯(lián)只是一個(gè)意外的副作用。new Foo() 只是間接完成了我們的目標(biāo):一個(gè)關(guān)聯(lián)到其他對(duì)象的新對(duì)象坠韩。

28. 技術(shù)

function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 創(chuàng)建一個(gè)新原型對(duì)象
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!

Object(..) 并沒(méi)有“構(gòu)造” a1距潘,對(duì)吧?看起來(lái)應(yīng)該是 Foo()“構(gòu)造”了它只搁。大部分開(kāi)發(fā)者都認(rèn)為是 Foo() 執(zhí)行了構(gòu)造工作音比,但是問(wèn)題在于,如果你認(rèn)為 “constructor” 表示“由……構(gòu)造”的話须蜗,a1.constructor 應(yīng)該是 Foo硅确,但是它并不是 Foo
到底怎么回事明肮? a1 并沒(méi)有 .constructor 屬性菱农,所以它會(huì)委托 [[Prototype]] 鏈上的 Foo.prototype。但是這個(gè)對(duì)象也沒(méi)有 .constructor 屬性(不過(guò)默認(rèn)的 Foo.prototype 對(duì)象有這個(gè)屬性J凉馈)循未,所以它會(huì)繼續(xù)委托,這次會(huì)委托給委托鏈頂端的 Object.prototype秫舌。這個(gè)對(duì)象有 .constructor 屬性的妖,指向內(nèi)置的 Object(..) 函數(shù)。

  • 當(dāng)然足陨,你可以給 Foo.prototype 添加一個(gè) .constructor 屬性嫂粟,不過(guò)這需要手動(dòng)添加一個(gè)符合正常行為的不可枚舉(參見(jiàn)第 3 章)屬性。
    舉例來(lái)說(shuō):
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 創(chuàng)建一個(gè)新原型對(duì)象
// 需要在 Foo.prototype 上“修復(fù)”丟失的 .constructor 屬性
// 新對(duì)象屬性起到 Foo.prototype 的作用
// 關(guān)于 defineProperty(..)墨缘,參見(jiàn)第 3 章
Object.defineProperty( Foo.prototype, "constructor" , {
  enumerable: false,
  writable: true,
  configurable: true,
  value: Foo // 讓 .constructor 指向 Foo
} );

修復(fù) .constructor 需要很多手動(dòng)操作星虹。所有這些工作都是源于把 “constructor” 錯(cuò)誤地理解為“由……構(gòu)造”,這個(gè)誤解的代價(jià)實(shí)在太高了镊讼。

實(shí)際上宽涌,對(duì)象的 .constructor 會(huì)默認(rèn)指向一個(gè)函數(shù),這個(gè)函數(shù)可以通過(guò)對(duì)象的 .prototype 引用蝶棋⌒读粒“constructor” 和 “prototype” 這兩個(gè)詞本身的含義可能適用也可能不適用。最好的辦法是記住這一點(diǎn) “constructor 并不表示被構(gòu)造”玩裙。

.constructor 并不是一個(gè)不可變屬性兼贸。它是不可枚舉(參見(jiàn)上面的代碼)的段直,但是它的值是可寫的(可以被修改)。此外寝受,你可以給任意 [[Prototype]] 鏈中的任意對(duì)象添加一個(gè)名為 constructor 的屬性或者對(duì)其進(jìn)行修改坷牛,你可以任意對(duì)其賦值。

  • ES6 添加了輔助函數(shù) Object.setPrototypeOf(..)很澄,可以用標(biāo)準(zhǔn)并且可靠的方法來(lái)修
    改關(guān)聯(lián)京闰。
    我們來(lái)對(duì)比一下兩種把 Bar.prototype 關(guān)聯(lián)到 Foo.prototype 的方法:
// ES6 之前需要拋棄默認(rèn)的 Bar.prototype
Bar.ptototype = Object.create( Foo.prototype );
// ES6 開(kāi)始可以直接修改現(xiàn)有的 Bar.prototype
Object.setPrototypeOf( Bar.prototype, Foo.prototype );

如果忽略掉 Object.create(..) 方法帶來(lái)的輕微性能損失(拋棄的對(duì)象需要進(jìn)行垃圾回收),它實(shí)際上比 ES6 及其之后的方法更短而且可讀性更高甩苛。不過(guò)無(wú)論如何蹂楣,這是兩種完全不同的語(yǔ)法。

29. 原型 - 小結(jié)

如果要訪問(wèn)對(duì)象中并不存在的一個(gè)屬性讯蒲,[[Get]] 操作(參見(jiàn)第 3 章)就會(huì)查找對(duì)象內(nèi)部 [[Prototype]] 關(guān)聯(lián)的對(duì)象痊土。這個(gè)關(guān)聯(lián)關(guān)系實(shí)際上定義了一條“原型鏈”(有點(diǎn)像嵌套的作用域鏈),在查找屬性時(shí)會(huì)對(duì)它進(jìn)行遍歷墨林。

所有普通對(duì)象都有內(nèi)置的 Object.prototype赁酝,指向原型鏈的頂端(比如說(shuō)全局作用域),如果在原型鏈中找不到指定的屬性就會(huì)停止旭等。toString()酌呆、valueOf() 和其他一些通用的功能都存在于 Object.prototype 對(duì)象上,因此語(yǔ)言中所有的對(duì)象都可以使用它們搔耕。

關(guān)聯(lián)兩個(gè)對(duì)象最常用的方法是使用 new 關(guān)鍵詞進(jìn)行函數(shù)調(diào)用隙袁,在調(diào)用的 4 個(gè)步驟(第 2章)中會(huì)創(chuàng)建一個(gè)關(guān)聯(lián)其他對(duì)象的新對(duì)象。

4 個(gè)步驟:
創(chuàng)建(或者說(shuō)構(gòu)造)一個(gè)全新的對(duì)象弃榨。
這個(gè)新對(duì)象會(huì)被執(zhí)行 [[ 原型 ]] 連接菩收。
這個(gè)新對(duì)象會(huì)綁定到函數(shù)調(diào)用的 this。
如果函數(shù)沒(méi)有返回其他對(duì)象鲸睛,那么 new 表達(dá)式中的函數(shù)調(diào)用會(huì)自動(dòng)返回這個(gè)新對(duì)象娜饵。

使用 new 調(diào)用函數(shù)時(shí)會(huì)把新對(duì)象的 .prototype 屬性關(guān)聯(lián)到“其他對(duì)象”。帶 new 的函數(shù)調(diào)用通常被稱為“構(gòu)造函數(shù)調(diào)用”官辈,盡管它們實(shí)際上和傳統(tǒng)面向類語(yǔ)言中的類構(gòu)造函數(shù)不一樣箱舞。

雖然這些 JavaScript 機(jī)制和傳統(tǒng)面向類語(yǔ)言中的“類初始化”和“類繼承”很相似,但
JavaScript 中的機(jī)制有一個(gè)核心區(qū)別钧萍,那就是不會(huì)進(jìn)行復(fù)制,對(duì)象之間是通過(guò)內(nèi)部的 [[Prototype]] 鏈關(guān)聯(lián)的政鼠。

出于各種原因风瘦,以“繼承”結(jié)尾的術(shù)語(yǔ)(包括“原型繼承”)和其他面向?qū)ο蟮男g(shù)語(yǔ)都無(wú)
法幫助你理解 JavaScript 的真實(shí)機(jī)制(不僅僅是限制我們的思維模式)。

相比之下公般,“委托”是一個(gè)更合適的術(shù)語(yǔ)万搔,因?yàn)閷?duì)象之間的關(guān)系不是復(fù)制而是委托胡桨。

30. 行為委托

  • 面向?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();

子類 Bar 繼承了父類 Foo,然后生成了 b1b2 兩個(gè)實(shí)例瞬雹。b1 委托了 Bar.prototype昧谊,后者委托了 Foo.prototype

  • 對(duì)象關(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();
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末酗捌,一起剝皮案震驚了整個(gè)濱河市呢诬,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌胖缤,老刑警劉巖尚镰,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異哪廓,居然都是意外死亡狗唉,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門涡真,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)分俯,“玉大人,你說(shuō)我怎么就攤上這事哆料「准簦” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵剧劝,是天一觀的道長(zhǎng)橄登。 經(jīng)常有香客問(wèn)我,道長(zhǎng)讥此,這世上最難降的妖魔是什么拢锹? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮萄喳,結(jié)果婚禮上卒稳,老公的妹妹穿的比我還像新娘。我一直安慰自己他巨,他們只是感情好充坑,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著染突,像睡著了一般捻爷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上份企,一...
    開(kāi)封第一講書(shū)人閱讀 51,688評(píng)論 1 305
  • 那天也榄,我揣著相機(jī)與錄音,去河邊找鬼。 笑死甜紫,一個(gè)胖子當(dāng)著我的面吹牛降宅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播囚霸,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼腰根,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了拓型?” 一聲冷哼從身側(cè)響起额嘿,我...
    開(kāi)封第一講書(shū)人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎吨述,沒(méi)想到半個(gè)月后岩睁,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡揣云,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年捕儒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片邓夕。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡刘莹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出焚刚,到底是詐尸還是另有隱情点弯,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布矿咕,位于F島的核電站抢肛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏碳柱。R本人自食惡果不足惜捡絮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望莲镣。 院中可真熱鬧福稳,春花似錦、人聲如沸瑞侮。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)半火。三九已至越妈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間钮糖,已是汗流浹背梅掠。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瓤檐。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像娱节,于是被迫代替她去往敵國(guó)和親挠蛉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

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