在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ HTML/ 面向?qū)ο缶幊讨?ECMAScript 實(shí)現(xiàn)
代碼復(fù)用模式(避免篇)
S.O.L.I.D 五大原則之接口隔離原則 ISP
設(shè)計(jì)模式之狀態(tài)模式
JavaScript 核心(晉級(jí)高手必讀篇)
設(shè)計(jì)模式之建造者模式
JavaScript 與 DOM(上)——也適用于新手
設(shè)計(jì)模式之中介者模式
設(shè)計(jì)模式之裝飾者模式
設(shè)計(jì)模式之模板方法
設(shè)計(jì)模式之外觀模式
強(qiáng)大的原型和原型鏈
設(shè)計(jì)模式之構(gòu)造函數(shù)模式
揭秘命名函數(shù)表達(dá)式
深入理解J avaScript 系列(結(jié)局篇)
執(zhí)行上下文(Execution Contexts)
函數(shù)(Functions)
《你真懂 JavaScript 嗎?》答案詳解
設(shè)計(jì)模式之適配器模式
設(shè)計(jì)模式之組合模式
設(shè)計(jì)模式之命令模式
S.O.L.I.D 五大原則之單一職責(zé) SRP
編寫高質(zhì)量 JavaScript 代碼的基本要點(diǎn)
求值策略
閉包(Closures)
對(duì)象創(chuàng)建模式(上篇)
This? Yes,this!
設(shè)計(jì)模式之代理模式
變量對(duì)象(Variable Object)
S.O.L.I.D 五大原則之里氏替換原則 LSP
面向?qū)ο缶幊讨话憷碚?/span>
設(shè)計(jì)模式之單例模式
Function 模式(上篇)
S.O.L.I.D 五大原則之依賴倒置原則 DIP
設(shè)計(jì)模式之迭代器模式
立即調(diào)用的函數(shù)表達(dá)式
設(shè)計(jì)模式之享元模式
設(shè)計(jì)模式之原型模式
根本沒有“JSON 對(duì)象”這回事!
JavaScript 與 DOM(下)
面向?qū)ο缶幊讨?ECMAScript 實(shí)現(xiàn)
全面解析 Module 模式
對(duì)象創(chuàng)建模式(下篇)
設(shè)計(jì)模式之職責(zé)鏈模式
S.O.L.I.D 五大原則之開閉原則 OCP
設(shè)計(jì)模式之橋接模式
設(shè)計(jì)模式之策略模式
設(shè)計(jì)模式之觀察者模式
代碼復(fù)用模式(推薦篇)
作用域鏈(Scope Chain)
Function 模式(下篇)
設(shè)計(jì)模式之工廠模式

面向?qū)ο缶幊讨?ECMAScript 實(shí)現(xiàn)

介紹

本章是關(guān)于 ECMAScript 面向?qū)ο髮?shí)現(xiàn)的第 2 篇,第 1 篇我們討論的是概論和 CEMAScript 的比較,如果你還沒有讀第1篇,在進(jìn)行本章之前,我強(qiáng)烈建議你先讀一下第1篇,因?yàn)楸酒獙?shí)在太長(zhǎng)了(35頁(yè))。

注:由于篇幅太長(zhǎng)了,難免出現(xiàn)錯(cuò)誤,時(shí)刻保持修正中。

在概論里,我們延伸到了ECMAScript,現(xiàn)在,當(dāng)我們知道它OOP實(shí)現(xiàn)時(shí),我們?cè)賮頊?zhǔn)確定義一下:

ECMAScript是一種面向?qū)ο笳Z(yǔ)言,支持基于原型的委托式繼承。

我們將從最基本的數(shù)據(jù)類型來分析,首先要了解的是 ECMAScript 用原始值(primitive values)和對(duì)象(objects)來區(qū)分實(shí)體,因此有些文章里說的“在 JavaScript 里,一切都是對(duì)象”是錯(cuò)誤的(不完全對(duì)),原始值就是我們這里要討論的一些數(shù)據(jù)類型。

數(shù)據(jù)類型

雖然 ECMAScript 是可以動(dòng)態(tài)轉(zhuǎn)化類型的動(dòng)態(tài)弱類型語(yǔ)言,它還是有數(shù)據(jù)類型的。也就是說,一個(gè)對(duì)象要屬于一個(gè)實(shí)實(shí)在在的類型。標(biāo)準(zhǔn)規(guī)范里定義了 9 種數(shù)據(jù)類型,但只有 6 種是在 ECMAScript 程序里可以直接訪問的,它們是:Undefined、Null、Boolean、String、Number、Object。

另外 3 種類型只能在實(shí)現(xiàn)級(jí)別訪問(ECMAScript 對(duì)象是不能使用這些類型的)并用于規(guī)范來解釋一些操作行為、保存中間值。這 3 種類型是:Reference、List 和 Completion。

因此,Reference 是用來解釋 delete、typeof、this 這樣的操作符,并且包含一個(gè)基對(duì)象和一個(gè)屬性名稱;List 描述的是參數(shù)列表的行為(在 new 表達(dá)式和函數(shù)調(diào)用的時(shí)候);Completion 是用來解釋行為 break、continue、return 和 throw 語(yǔ)句的。

原始值類型

回頭來看 6 中用于 ECMAScript 程序的數(shù)據(jù)類型,前5種是原始值類型,包括 Undefined、Null、Boolean、String、Number、Object。 原始值類型例子:

var a = undefined;
var b = null;
var c = true;
var d = 'test';
var e = 10;

這些值是在底層上直接實(shí)現(xiàn)的,他們不是 object,所以沒有原型,沒有構(gòu)造函數(shù)。

大叔注:這些原生值和我們平時(shí)用的(Boolean、String、Number、Object)雖然名字上相似,但不是同一個(gè)東西。所以 typeof(true)和 typeof(Boolean)結(jié)果是不一樣的,因?yàn)?typeof(Boolean)的結(jié)果是 function,所以函數(shù) Boolean、String、Number 是有原型的(下面的讀寫屬性章節(jié)也會(huì)提到)。

想知道數(shù)據(jù)是哪種類型用 typeof 是最好不過了,有個(gè)例子需要注意一下,如果用 typeof 來判斷 null 的類型,結(jié)果是 object,為什么呢?因?yàn)?null 的類型是定義為 Null 的。

alert(typeof null); // "object"

顯示"object"原因是因?yàn)橐?guī)范就是這么規(guī)定的:對(duì)于 Null 值的 typeof 字符串值返回"object“。

規(guī)范沒有想象解釋這個(gè),但是 Brendan Eich (JavaScript 發(fā)明人)注意到 null 相對(duì)于 undefined 大多數(shù)都是用于對(duì)象出現(xiàn)的地方,例如設(shè)置一個(gè)對(duì)象為空引用。但是有些文檔里有些氣人將之歸結(jié)為 bug,而且將該 bug 放在 Brendan Eich 也參與討論的 bug 列表里,結(jié)果就是任其自然,還是把 typeof null 的結(jié)果設(shè)置為 object(盡管 262-3 的標(biāo)準(zhǔn)是定義 null 的類型是 Null,262-5 已經(jīng)將標(biāo)準(zhǔn)修改為 null 的類型是 object 了)。

Object 類型

接著,Object 類型(不要和 Object 構(gòu)造函數(shù)混淆了,現(xiàn)在只討論抽象類型)是描述 ECMAScript 對(duì)象的唯一一個(gè)數(shù)據(jù)類型。

對(duì)象是一個(gè)包含key-value對(duì)的無序集合

對(duì)象的 key 值被稱為屬性,屬性是原始值和其他對(duì)象的容器。如果屬性的值是函數(shù)我們稱它為方法 。

例如:

var x = { // 對(duì)象"x"有3個(gè)屬性: a, b, c
  a: 10, // 原始值
  b: {z: 100}, // 對(duì)象"b"有一個(gè)屬性z
  c: function () { // 函數(shù)(方法)
    alert('method x.c');
  }
};  
alert(x.a); // 10
alert(x.b); // [object Object]
alert(x.b.z); // 100
x.c(); // 'method x.c'

動(dòng)態(tài)性

正如我們?cè)诘?17 章中指出的,ES 中的對(duì)象是完全動(dòng)態(tài)的。這意味著,在程序執(zhí)行的時(shí)候我們可以任意地添加,修改或刪除對(duì)象的屬性。

例如:

var foo = {x: 10};
// 添加新屬性
foo.y = 20;
console.log(foo); // {x: 10, y: 20}  
// 將屬性值修改為函數(shù)
foo.x = function () {
  console.log('foo.x');
};  
foo.x(); // 'foo.x'  
// 刪除屬性
delete foo.x;
console.log(foo); // {y: 20}

有些屬性不能被修改——(只讀屬性、已刪除屬性或不可配置的屬性)。 我們將稍后在屬性特性里講解。

另外,ES5 規(guī)范規(guī)定,靜態(tài)對(duì)象不能擴(kuò)展新的屬性,并且它的屬性頁(yè)不能刪除或者修改。他們是所謂的凍結(jié)對(duì)象,可以通過應(yīng)用 Object.freeze(o)方法得到。

var foo = {x: 10};  
// 凍結(jié)對(duì)象
Object.freeze(foo);
console.log(Object.isFrozen(foo)); // true  
// 不能修改
foo.x = 100;  
// 不能擴(kuò)展
foo.y = 200;  
// 不能刪除
delete foo.x;  
console.log(foo); // {x: 10}

在 ES5 規(guī)范里,也使用 Object.preventExtensions(o)方法防止擴(kuò)展,或者使用 Object.defineProperty(o)方法來定義屬性:

var foo = {x : 10};  
Object.defineProperty(foo, "y", {
  value: 20,
  writable: false, // 只讀
  configurable: false // 不可配置
});  
// 不能修改
foo.y = 200;  
// 不能刪除
delete foo.y; // false  
// 防治擴(kuò)展
Object.preventExtensions(foo);
console.log(Object.isExtensible(foo)); // false  
// 不能添加新屬性
foo.z = 30;  
console.log(foo); {x: 10, y: 20}

內(nèi)置對(duì)象、原生對(duì)象及宿主對(duì)象

有必要需要注意的是規(guī)范還區(qū)分了這內(nèi)置對(duì)象、元素對(duì)象和宿主對(duì)象。

內(nèi)置對(duì)象和元素對(duì)象是被 ECMAScript 規(guī)范定義和實(shí)現(xiàn)的,兩者之間的差異微不足道。所有 ECMAScript 實(shí)現(xiàn)的對(duì)象都是原生對(duì)象(其中一些是內(nèi)置對(duì)象、一些在程序執(zhí)行的時(shí)候創(chuàng)建,例如用戶自定義對(duì)象)。內(nèi)置對(duì)象是原生對(duì)象的一個(gè)子集、是在程序開始之前內(nèi)置到 ECMAScript 里的(例如,parseInt, Match 等)。所有的宿主對(duì)象是由宿主環(huán)境提供的,通常是瀏覽器,并可能包括如 window、alert 等。

注意,宿主對(duì)象可能是 ES 自身實(shí)現(xiàn)的,完全符合規(guī)范的語(yǔ)義。從這點(diǎn)來說,他們能稱為“原生宿主”對(duì)象(盡快很理論),不過規(guī)范沒有定義“原生宿主”對(duì)象的概念。

Boolean,String 和 Number 對(duì)象

另外,規(guī)范也定義了一些原生的特殊包裝類,這些對(duì)象是:

  1. 布爾對(duì)象
  2. 字符串對(duì)象
  3. 數(shù)字對(duì)象

這些對(duì)象的創(chuàng)建,是通過相應(yīng)的內(nèi)置構(gòu)造器創(chuàng)建,并且包含原生值作為其內(nèi)部屬性,這些對(duì)象可以轉(zhuǎn)換省原始值,反之亦然。

var c = new Boolean(true);
var d = new String('test');
var e = new Number(10);  
// 轉(zhuǎn)換成原始值
// 使用不帶new關(guān)鍵字的函數(shù)
с = Boolean(c);
d = String(d);
e = Number(e);  
// 重新轉(zhuǎn)換成對(duì)象
с = Object(c);
d = Object(d);
e = Object(e);

此外,也有對(duì)象是由特殊的內(nèi)置構(gòu)造函數(shù)創(chuàng)建:Function(函數(shù)對(duì)象構(gòu)造器)、Array(數(shù)組構(gòu)造器) RegExp(正則表達(dá)式構(gòu)造器)、Math(數(shù)學(xué)模塊)、 Date(日期的構(gòu)造器)等等,這些對(duì)象也是 Object 對(duì)象類型的值,他們彼此的區(qū)別是由內(nèi)部屬性管理的,我們?cè)谙旅嬗懻撨@些內(nèi)容。

字面量 Literal

對(duì)于三個(gè)對(duì)象的值:對(duì)象(object),數(shù)組(array)和正則表達(dá)式(regular expression),他們分別有簡(jiǎn)寫的標(biāo)示符稱為:對(duì)象初始化器、數(shù)組初始化器、和正則表達(dá)式初始化器:

// 等價(jià)于new Array(1, 2, 3);
// 或者array = new Array();
// array[0] = 1;
// array[1] = 2;
// array[2] = 3;
var array = [1, 2, 3];  
// 等價(jià)于
// var object = new Object();
// object.a = 1;
// object.b = 2;
// object.c = 3;
var object = {a: 1, b: 2, c: 3};  
// 等價(jià)于new RegExp("^\\d+$", "g")
var re = /^\d+$/g;

注意,如果上述三個(gè)對(duì)象進(jìn)行重新賦值名稱到新的類型上的話,那隨后的實(shí)現(xiàn)語(yǔ)義就是按照新賦值的類型來使用,例如在當(dāng)前的 Rhino 和老版本 SpiderMonkey 1.7 的實(shí)現(xiàn)上,會(huì)成功以 new 關(guān)鍵字的構(gòu)造器來創(chuàng)建對(duì)象,但有些實(shí)現(xiàn)(當(dāng)前 Spider/TraceMonkey)字面量的語(yǔ)義在類型改變以后卻不一定改變。

var getClass = Object.prototype.toString;  
Object = Number;  
var foo = new Object;
alert([foo, getClass.call(foo)]); // 0, "[object Number]"  
var bar = {};  
// Rhino, SpiderMonkey 1.7中 - 0, "[object Number]"
// 其它: still "[object Object]", "[object Object]"
alert([bar, getClass.call(bar)]);  
// Array也是一樣的效果
Array = Number;  
foo = new Array;
alert([foo, getClass.call(foo)]); // 0, "[object Number]"  
bar = [];  
// Rhino, SpiderMonkey 1.7中 - 0, "[object Number]"
// 其它: still "", "[object Object]"
alert([bar, getClass.call(bar)]);  
// 但對(duì)RegExp,字面量的語(yǔ)義是不被改變的。 semantics of the literal
// isn't being changed in all tested implementations  
RegExp = Number;  
foo = new RegExp;
alert([foo, getClass.call(foo)]); // 0, "[object Number]"  
bar = /(?!)/g;
alert([bar, getClass.call(bar)]); // /(?!)/g, "[object RegExp]"

正則表達(dá)式字面量和 RegExp 對(duì)象

注意,下面 2 個(gè)例子在第三版的規(guī)范里,正則表達(dá)式的語(yǔ)義都是等價(jià)的,regexp 字面量只在一句里存在,并且再解析階段創(chuàng)建,但 RegExp 構(gòu)造器創(chuàng)建的卻是新對(duì)象,所以這可能會(huì)導(dǎo)致出一些問題,如 lastIndex 的值在測(cè)試的時(shí)候結(jié)果是錯(cuò)誤的:

for (var k = 0; k < 4; k++) {
  var re = /ecma/g;
  alert(re.lastIndex); // 0, 4, 0, 4
  alert(re.test("ecmascript")); // true, false, true, false
}  
// 對(duì)比  
for (var k = 0; k < 4; k++) {
  var re = new RegExp("ecma", "g");
  alert(re.lastIndex); // 0, 0, 0, 0
  alert(re.test("ecmascript")); // true, true, true, true
}

注:不過這些問題在第 5 版的 ES 規(guī)范都已經(jīng)修正了,不管是基于字面量的還是構(gòu)造器的,正則都是創(chuàng)建新對(duì)象。

關(guān)聯(lián)數(shù)組

各種文字靜態(tài)討論,JavaScript 對(duì)象(經(jīng)常是用對(duì)象初始化器{}來創(chuàng)建)被稱為哈希表哈希表或其它簡(jiǎn)單的稱謂:哈希(Ruby 或 Perl 里的概念), 管理數(shù)組(PHP 里的概念),詞典 (Python 里的概念)等。

只有這樣的術(shù)語(yǔ),主要是因?yàn)樗麄兊慕Y(jié)構(gòu)都是相似的,就是使用“鍵-值”對(duì)來存儲(chǔ)對(duì)象,完全符合“關(guān)聯(lián)數(shù)組 ”或“哈希表 ”理論定義的數(shù)據(jù)結(jié)構(gòu)。 此外,哈希表抽象數(shù)據(jù)類型通常是在實(shí)現(xiàn)層面使用。

但是,盡管術(shù)語(yǔ)上來描述這個(gè)概念,但實(shí)際上這個(gè)是錯(cuò)誤,從 ECMAScript 來看:ECMAScript 只有一個(gè)對(duì)象以及類型以及它的子類型,這和“鍵-值”對(duì)存儲(chǔ)沒有什么區(qū)別,因此在這上面沒有特別的概念。 因?yàn)槿魏螌?duì)象的內(nèi)部屬性都可以存儲(chǔ)為鍵-值”對(duì):

var a = {x: 10};
a['y'] = 20;
a.z = 30;  
var b = new Number(1);
b.x = 10;
b.y = 20;
b['z'] = 30;  
var c = new Function('');
c.x = 10;
c.y = 20;
c['z'] = 30;  
// 等等,任意對(duì)象的子類型"subtype"

此外,由于在 ECMAScript 中對(duì)象可以是空的,所以"hash"的概念在這里也是不正確的:

Object.prototype.x = 10;  
var a = {}; // 創(chuàng)建空"hash"  
alert(a["x"]); // 10, 但不為空
alert(a.toString); // function  
a["y"] = 20; // 添加新的鍵值對(duì)到 "hash"
alert(a["y"]); // 20  
Object.prototype.y = 20; // 添加原型屬性  
delete a["y"]; // 刪除
alert(a["y"]); // 但這里key和value依然有值 – 20

請(qǐng)注意,ES5 標(biāo)準(zhǔn)可以讓我們創(chuàng)建沒原型的對(duì)象(使用 Object.create(null)方法實(shí)現(xiàn))對(duì),從這個(gè)角度來說,這樣的對(duì)象可以稱之為哈希表:

var aHashTable = Object.create(null);
console.log(aHashTable.toString); // 未定義

此外,一些屬性有特定的 getter/setter 方法??,所以也可能導(dǎo)致混淆這個(gè)概念:

var a = new String("foo");
a['length'] = 10;
alert(a['length']); // 3

然而,即使認(rèn)為“哈?!笨赡苡幸粋€(gè)“原型”(例如,在 Ruby 或 Python 里委托哈希對(duì)象的類),在 ECMAScript 里,這個(gè)術(shù)語(yǔ)也是不對(duì)的,因?yàn)?2 個(gè)表示法之間沒有語(yǔ)義上的區(qū)別(即用點(diǎn)表示法 a.b 和 a["b"]表示法)。

在 ECMAScript 中的“property 屬性”的概念語(yǔ)義上和"key"、數(shù)組索引、方法沒有分開的,這里所有對(duì)象的屬性讀寫都要遵循統(tǒng)一的規(guī)則:檢查原型鏈。

在下面 Ruby 的例子中,我們可以看到語(yǔ)義上的區(qū)別:

a = {}
a.class # Hash
a.length # 0  
\# new "key-value" pair
a['length'] = 10;
\# 語(yǔ)義上,用點(diǎn)訪問的是屬性或方法,而不是key  
a.length # 1  
\# 而索引器訪問訪問的是hash里的key  
a['length'] # 10  
\# 就類似于在現(xiàn)有對(duì)象上動(dòng)態(tài)聲明Hash類
\# 然后聲明新屬性或方法  
class Hash
  def z
    100
  end
end
\# 新屬性可以訪問  
a.z # 100  
\# 但不是"key"  
a['z'] # nil

ECMA-262-3標(biāo)準(zhǔn)并沒有定義“哈?!保ㄒ约邦愃疲┑母拍睢5?,有這樣的結(jié)構(gòu)理論的話,那可能以此命名的對(duì)象。

對(duì)象轉(zhuǎn)換

將對(duì)象轉(zhuǎn)化成原始值可以用 valueOf 方法,正如我們所說的,當(dāng)函數(shù)的構(gòu)造函數(shù)調(diào)用做為 function(對(duì)于某些類型的),但如果不用 new 關(guān)鍵字就是將對(duì)象轉(zhuǎn)化成原始值,就相當(dāng)于隱式的 valueOf 方法調(diào)用:

var a = new Number(1);
var primitiveA = Number(a); // 隱式"valueOf"調(diào)用
var alsoPrimitiveA = a.valueOf(); // 顯式調(diào)用  
alert([
  typeof a, // "object"
  typeof primitiveA, // "number"
  typeof alsoPrimitiveA // "number"
]);

這種方式允許對(duì)象參與各種操作,例如:

var a = new Number(1);
var b = new Number(2);  
alert(a + b); // 3  
// 甚至  
var c = {
  x: 10,
  y: 20,
  valueOf: function () {
    return this.x + this.y;
  }
};  
var d = {
  x: 30,
  y: 40,
  // 和c的valueOf功能一樣
  valueOf: c.valueOf
};  
alert(c + d); // 100

valueOf 的默認(rèn)值會(huì)根據(jù)根據(jù)對(duì)象的類型改變(如果不被覆蓋的話),對(duì)某些對(duì)象,他返回的是 this——例如:Object.prototype.valueOf(),還有計(jì)算型的值:Date.prototype.valueOf()返回的是日期時(shí)間:

var a = {};
alert(a.valueOf() === a); // true, "valueOf"返回this  
var d = new Date();
alert(d.valueOf()); // time
alert(d.valueOf() === d.getTime()); // true

此外,對(duì)象還有一個(gè)更原始的代表性——字符串展示。 這個(gè) toString 方法是可靠的,它在某些操作上是自動(dòng)使用的:

var a = {
  valueOf: function () {
    return 100;
  },
  toString: function () {
    return '__test';
  }
};  
// 這個(gè)操作里,toString方法自動(dòng)調(diào)用
alert(a); // "__test"  
// 但是這里,調(diào)用的卻是valueOf()方法
alert(a + 10); // 110  
// 但,一旦valueOf刪除以后
// toString又可以自動(dòng)調(diào)用了
delete a.valueOf;
alert(a + 10); // "_test10"

Object.prototype 上定義的 toString 方法具有特殊意義,它返回的我們下面將要討論的內(nèi)部[[Class]]屬性值。

和轉(zhuǎn)化成原始值(ToPrimitive)相比,將值轉(zhuǎn)化成對(duì)象類型也有一個(gè)轉(zhuǎn)化規(guī)范(ToObject)。

一個(gè)顯式方法是使用內(nèi)置的 Object 構(gòu)造函數(shù)作為 function 來調(diào)用 ToObject(有些類似通過 new 關(guān)鍵字也可以):

var n = Object(1); // [object Number]
var s = Object('test'); // [object String]  
// 一些類似,使用new操作符也可以
var b = new Object(true); // [object Boolean]  
// 應(yīng)用參數(shù)new Object的話創(chuàng)建的是簡(jiǎn)單對(duì)象
var o = new Object(); // [object Object]  
// 如果參數(shù)是一個(gè)現(xiàn)有的對(duì)象
// 那創(chuàng)建的結(jié)果就是簡(jiǎn)單返回該對(duì)象
var a = [];
alert(a === new Object(a)); // true
alert(a === Object(a)); // true

關(guān)于調(diào)用內(nèi)置構(gòu)造函數(shù),使用還是不適用 new 操作符沒有通用規(guī)則,取決于構(gòu)造函數(shù)。 例如 Array 或 Function 當(dāng)使用 new 操作符的構(gòu)造函數(shù)或者不使用 new 操作符的簡(jiǎn)單函數(shù)使用產(chǎn)生相同的結(jié)果的:

var a = Array(1, 2, 3); // [object Array]
var b = new Array(1, 2, 3); // [object Array]
var c = [1, 2, 3]; // [object Array]  
var d = Function(''); // [object Function]
var e = new Function(''); // [object Function]

有些操作符使用的時(shí)候,也有一些顯示和隱式轉(zhuǎn)化:

var a = 1;
var b = 2;   
// 隱式
var c = a + b; // 3, number
var d = a + b + '5' // "35", string  
// 顯式
var e = '10'; // "10", string
var f = +e; // 10, number
var g = parseInt(e, 10); // 10, number  
// 等等

屬性的特性

所有的屬性(property) 都可以有很多特性(attributes)。

  1. {ReadOnly}——忽略向?qū)傩再x值的寫操作嘗,但只讀屬性可以由宿主環(huán)境行為改變——也就是說不是“恒定值” ;
  2. {DontEnum}——屬性不能被 for.in 循環(huán)枚舉;
  3. {DontDelete}——糊了 delete 操作符的行為被忽略(即刪不掉);
  4. {Internal}——內(nèi)部屬性,沒有名字(僅在實(shí)現(xiàn)層面使用),ECMAScript 里無法訪問這樣的屬性。

注意,在 ES5 里{ReadOnly},{DontEnum}和{DontDelete}被重新命名為[[Writable]],[[Enumerable]]和[[Configurable]],可以手工通過 Object.defineProperty 或類似的方法來管理這些屬性。

var foo = {};  
Object.defineProperty(foo, "x", {
  value: 10,
  writable: true, // 即{ReadOnly} = false
  enumerable: false, // 即{DontEnum} = true
  configurable: true // 即{DontDelete} = false
});  
console.log(foo.x); // 10  
// 通過descriptor獲取特性集attributes
var desc = Object.getOwnPropertyDescriptor(foo, "x");  
console.log(desc.enumerable); // false
console.log(desc.writable); // true
// 等等

內(nèi)部屬性和方法

對(duì)象也可以有內(nèi)部屬性(實(shí)現(xiàn)層面的一部分),并且 ECMAScript 程序無法直接訪問(但是下面我們將看到,一些實(shí)現(xiàn)允許訪問一些這樣的屬性)。 這些屬性通過嵌套的中括號(hào)[[ ]]進(jìn)行訪問。我們來看其中的一些,這些屬性的描述可以到規(guī)范里查閱到。

每個(gè)對(duì)象都應(yīng)該實(shí)現(xiàn)如下內(nèi)部屬性和方法:

  1. [[Prototype]]——對(duì)象的原型(將在下面詳細(xì)介紹)
  2. [[Class]]——字符串對(duì)象的一種表示(例如,Object Array ,F(xiàn)unction Object,F(xiàn)unction等);用來區(qū)分對(duì)象
  3. [[Get]]——獲得屬性值的方法
  4. [[Put]]——設(shè)置屬性值的方法
  5. [[CanPut]]——檢查屬性是否可寫
  6. [[HasProperty]]——檢查對(duì)象是否已經(jīng)擁有該屬性
  7. [[Delete]]——從對(duì)象刪除該屬性
  8. [[DefaultValue]]返回對(duì)象對(duì)于的原始值(調(diào)用 valueOf 方法,某些對(duì)象可能會(huì)拋出 TypeError 異常)。

通過 Object.prototype.toString()方法可以間接得到內(nèi)部屬性[[Class]]的值,該方法應(yīng)該返回下列字符串: "[object " + [[Class]] + "]" 。例如:

var getClass = Object.prototype.toString;  
getClass.call({}); // [object Object]
getClass.call([]); // [object Array]
getClass.call(new Number(1)); // [object Number]
// 等等

這個(gè)功能通常是用來檢查對(duì)象用的,但規(guī)范上說宿主對(duì)象的[[Class]]可以為任意值,包括內(nèi)置對(duì)象的[[Class]]屬性的值,所以理論上來看是不能 100%來保證準(zhǔn)確的。例如,document.childNodes.item(...)方法的[[Class]]屬性,在IE里返回"String",但其它實(shí)現(xiàn)里返回的確實(shí)"Function"。

// in IE - "String", in other - "Function"
alert(getClass.call(document.childNodes.item));

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

因此,正如我們上面提到的,在 ECMAScript 中的對(duì)象是通過所謂的構(gòu)造函數(shù)來創(chuàng)建的。

Constructor is a function that creates and initializes the newly created object.
構(gòu)造函數(shù)是一個(gè)函數(shù),用來創(chuàng)建并初始化新創(chuàng)建的對(duì)象。

對(duì)象創(chuàng)建(內(nèi)存分配)是由構(gòu)造函數(shù)的內(nèi)部方法[[Construct]]負(fù)責(zé)的。該內(nèi)部方法的行為是定義好的,所有的構(gòu)造函數(shù)都是使用該方法來為新對(duì)象分配內(nèi)存的。

而初始化是通過新建對(duì)象上下上調(diào)用該函數(shù)來管理的,這是由構(gòu)造函數(shù)的內(nèi)部方法[[Call]]來負(fù)責(zé)任的。

注意,用戶代碼只能在初始化階段訪問,雖然在初始化階段我們可以返回不同的對(duì)象(忽略第一階段創(chuàng)建的 tihs 對(duì)象):

function A() {
  // 更新新創(chuàng)建的對(duì)象
  this.x = 10;
  // 但返回的是不同的對(duì)象
  return [1, 2, 3];
}  
var a = new A();
console.log(a.x, a); undefined, [1, 2, 3]

引用 15 章函數(shù)——?jiǎng)?chuàng)建函數(shù)的算法小節(jié),我們可以看到該函數(shù)是一個(gè)原生對(duì)象,包含[[Construct]] ]和[[Call]] ]屬性以及顯示的 prototype 原型屬性——未來對(duì)象的原型(注:NativeObject 是對(duì)于 native object 原生對(duì)象的約定,在下面的偽代碼中使用)。

F = new NativeObject();  
F.[[Class]] = "Function"  
.... // 其它屬性  
F.[[Call]] = <reference to function> // function自身  
F.[[Construct]] = internalConstructor // 普通的內(nèi)部構(gòu)造函數(shù)  
.... // 其它屬性  
// F構(gòu)造函數(shù)創(chuàng)建的對(duì)象原型
__objectPrototype = {};
__objectPrototype.constructor = F // {DontEnum}
F.prototype = __objectPrototype

[[Call]] ]是除[[Class]]屬性(這里等同于"Function" )之外區(qū)分對(duì)象的主要方式,因此,對(duì)象的內(nèi)部[[Call]]屬性作為函數(shù)調(diào)用。 這樣的對(duì)象用 typeof 運(yùn)算操作符的話返回的是"function"。然而它主要是和原生對(duì)象有關(guān),有些情況的實(shí)現(xiàn)在用 typeof 獲取值的是不一樣的,例如:window.alert (...)在 IE 中的效果:

// IE瀏覽器中 - "Object", "object", 其它瀏覽器 - "Function", "function"
alert(Object.prototype.toString.call(window.alert));
alert(typeof window.alert); // "Object"

內(nèi)部方法[[Construct]]是通過使用帶new運(yùn)算符的構(gòu)造函數(shù)來激活的,正如我們所說的這個(gè)方法是負(fù)責(zé)內(nèi)存分配和對(duì)象創(chuàng)建的。如果沒有參數(shù),調(diào)用構(gòu)造函數(shù)的括號(hào)也可以省略:

function A(x) { // constructor А
  this.x = x || 10;
}  
// 不傳參數(shù)的話,括號(hào)也可以省略
var a = new A; // or new A();
alert(a.x); // 10  
// 顯式傳入?yún)?shù)x
var b = new A(20);
alert(b.x); // 20

我們也知道,構(gòu)造函數(shù)(初始化階段)里的 this 被設(shè)置為新創(chuàng)建的對(duì)象 。

讓我們研究一下對(duì)象創(chuàng)建的算法。

對(duì)象創(chuàng)建的算法

內(nèi)部方法[[Construct]] 的行為可以描述成如下:

F.[[Construct]](initialParameters):   
O = new NativeObject();  
// 屬性[[Class]]被設(shè)置為"Object"
O.[[Class]] = "Object"  
// 引用F.prototype的時(shí)候獲取該對(duì)象g
var __objectPrototype = F.prototype;  
// 如果__objectPrototype是對(duì)象,就:
O.[[Prototype]] = __objectPrototype
// 否則:
O.[[Prototype]] = Object.prototype;
// 這里O.[[Prototype]]是Object對(duì)象的原型  
// 新創(chuàng)建對(duì)象初始化的時(shí)候應(yīng)用了F.[[Call]]
// 將this設(shè)置為新創(chuàng)建的對(duì)象O
// 參數(shù)和F里的initialParameters是一樣的
R = F.[[Call]](initialParameters); this === O;
// 這里R是[[Call]]的返回值
// 在JS里看,像這樣:
// R = F.apply(O, initialParameters);  
// 如果R是對(duì)象
return R
// 否則
return O

請(qǐng)注意兩個(gè)主要特點(diǎn):

  1. 首先,新創(chuàng)建對(duì)象的原型是從當(dāng)前時(shí)刻函數(shù)的 prototype 屬性獲取的(這意味著同一個(gè)構(gòu)造函數(shù)創(chuàng)建的兩個(gè)創(chuàng)建對(duì)象的原型可以不同是因?yàn)楹瘮?shù)的 prototype 屬性也可以不同)。
  2. 其次,正如我們上面提到的,如果在對(duì)象初始化的時(shí)候,[[Call]]返回的是對(duì)象,這恰恰是用于整個(gè) new 操作符的結(jié)果:
function A() {}
A.prototype.x = 10;  
var a = new A();
alert(a.x); // 10 – 從原型上得到  
// 設(shè)置.prototype屬性為新對(duì)象
// 為什么顯式聲明.constructor屬性將在下面說明
A.prototype = {
  constructor: A,
  y: 100
};  
var b = new A();
// 對(duì)象"b"有了新屬性
alert(b.x); // undefined
alert(b.y); // 100 – 從原型上得到  
// 但a對(duì)象的原型依然可以得到原來的結(jié)果
alert(a.x); // 10 - 從原型上得到  
function B() {
  this.x = 10;
  return new Array();
}  
// 如果"B"構(gòu)造函數(shù)沒有返回(或返回this)
// 那么this對(duì)象就可以使用,但是下面的情況返回的是array
var b = new B();
alert(b.x); // undefined
alert(Object.prototype.toString.call(b)); // [object Array]

讓我們來詳細(xì)了解一下原型

原型

每個(gè)對(duì)象都有一個(gè)原型(一些系統(tǒng)對(duì)象除外)。原型通信是通過內(nèi)部的、隱式的、不可直接訪問[[Prototype]]原型屬性來進(jìn)行的,原型可以是一個(gè)對(duì)象,也可以是 null 值。

屬性構(gòu)造函數(shù)(Property constructor)

上面的例子有有 2 個(gè)重要的知識(shí)點(diǎn),第一個(gè)是關(guān)于函數(shù)的 constructor 屬性的 prototype 屬性,在函數(shù)創(chuàng)建的算法里,我們知道 constructor 屬性在函數(shù)創(chuàng)建階段被設(shè)置為函數(shù)的 prototype 屬性,constructor 屬性的值是函數(shù)自身的重要引用:

function A() {}
var a = new A();
alert(a.constructor); // function A() {}, by delegation
alert(a.constructor === A); // true

通常在這種情況下,存在著一個(gè)誤區(qū):constructor 構(gòu)造屬性作為新創(chuàng)建對(duì)象自身的屬性是錯(cuò)誤的,但是,正如我們所看到的的,這個(gè)屬性屬于原型并且通過繼承來訪問對(duì)象。

通過繼承 constructor 屬性的實(shí)例,可以間接得到的原型對(duì)象的引用:

function A() {}
A.prototype.x = new Number(10);
var a = new A();
alert(a.constructor.prototype); // [object Object]  
alert(a.x); // 10, 通過原型
// 和a.[[Prototype]].x效果一樣
alert(a.constructor.prototype.x); // 10  
alert(a.constructor.prototype.x === a.x); // true

但請(qǐng)注意,函數(shù)的 constructor 和 prototype 屬性在對(duì)象創(chuàng)建以后都可以重新定義的。在這種情況下,對(duì)象失去上面所說的機(jī)制。如果通過函數(shù)的 prototype 屬性去編輯元素的 prototype 原型的話(添加新對(duì)象或修改現(xiàn)有對(duì)象),實(shí)例上將看到新添加的屬性。

然而,如果我們徹底改變函數(shù)的 prototype 屬性(通過分配一個(gè)新的對(duì)象),那原始構(gòu)造函數(shù)的引用就是丟失,這是因?yàn)槲覀儎?chuàng)建的對(duì)象不包括 constructor 屬性:

function A() {}
A.prototype = {
  x: 10
};   
var a = new A();
alert(a.x); // 10
alert(a.constructor === A); // false!

因此,對(duì)函數(shù)的原型引用需要手工恢復(fù):

function A() {}
A.prototype = {
  constructor: A,
  x: 10
};  
var a = new A();
alert(a.x); // 10
alert(a.constructor === A); // true

注意雖然手動(dòng)恢復(fù)了 constructor 屬性,和原來丟失的原型相比,{DontEnum}特性沒有了,也就是說 A.prototype 里的 for..in 循環(huán)語(yǔ)句不支持了,不過第 5 版規(guī)范里,通過[[Enumerable]] 特性提供了控制可枚舉狀態(tài) enumerable 的能力。

var foo = {x: 10};  
Object.defineProperty(foo, "y", {
  value: 20,
  enumerable: false // aka {DontEnum} = true
});  
console.log(foo.x, foo.y); // 10, 20  
for (var k in foo) {
  console.log(k); // only "x"
}  
var xDesc = Object.getOwnPropertyDescriptor(foo, "x");
var yDesc = Object.getOwnPropertyDescriptor(foo, "y");  
console.log(
  xDesc.enumerable, // true
  yDesc.enumerable  // false
);

顯式 prototype 和隱式[[Prototype]]屬性

通常,一個(gè)對(duì)象的原型通過函數(shù)的 prototype 屬性顯式引用是不正確的,他引用的是同一個(gè)對(duì)象,對(duì)象的[[Prototype]]屬性:

a.[[Prototype]] ----> Prototype <---- A.prototype

此外, 實(shí)例的[[Prototype]]值確實(shí)是在構(gòu)造函數(shù)的 prototype 屬性上獲取的。

然而,提交 prototype 屬性不會(huì)影響已經(jīng)創(chuàng)建對(duì)象的原型(只有在構(gòu)造函數(shù)的 prototype 屬性改變的時(shí)候才會(huì)影響到),就是說新創(chuàng)建的對(duì)象才有有新的原型,而已創(chuàng)建對(duì)象還是引用到原來的舊原型(這個(gè)原型已經(jīng)不能被再被修改了)。

// 在修改A.prototype原型之前的情況
a.[[Prototype]] ----> Prototype <---- A.prototype  
// 修改之后
A.prototype ----> New prototype // 新對(duì)象會(huì)擁有這個(gè)原型
a.[[Prototype]] ----> Prototype // 引導(dǎo)的原來的原型上

例如:

function A() {}
A.prototype.x = 10;  
var a = new A();
alert(a.x); // 10  
A.prototype = {
  constructor: A,
  x: 20
  y: 30
};  
// 對(duì)象a是通過隱式的[[Prototype]]引用從原油的prototype上獲取的值
alert(a.x); // 10
alert(a.y) // undefined  
var b = new A();  
// 但新對(duì)象是從新原型上獲取的值
alert(b.x); // 20
alert(b.y) // 30

因此,有的文章說“動(dòng)態(tài)修改原型將影響所有的對(duì)象都會(huì)擁有新的原型”是錯(cuò)誤的,新原型僅僅在原型修改以后的新創(chuàng)建對(duì)象上生效。

這里的主要規(guī)則是:對(duì)象的原型是對(duì)象的創(chuàng)建的時(shí)候創(chuàng)建的,并且在此之后不能修改為新的對(duì)象,如果依然引用到同一個(gè)對(duì)象,可以通過構(gòu)造函數(shù)的顯式 prototype 引用,對(duì)象創(chuàng)建以后,只能對(duì)原型的屬性進(jìn)行添加或修改。

非標(biāo)準(zhǔn)的proto屬性

然而,有些實(shí)現(xiàn)(例如 SpiderMonkey),提供了不標(biāo)準(zhǔn)的__proto__顯式屬性來引用對(duì)象的原型:

function A() {}
A.prototype.x = 10;  
var a = new A();
alert(a.x); // 10  
var __newPrototype = {
  constructor: A,
  x: 20,
  y: 30
};  
// 引用到新對(duì)象
A.prototype = __newPrototype;  
var b = new A();
alert(b.x); // 20
alert(b.y); // 30  
// "a"對(duì)象使用的依然是舊的原型
alert(a.x); // 10
alert(a.y); // undefined  
// 顯式修改原型
a.__proto__ = __newPrototype;  
// 現(xiàn)在"а"對(duì)象引用的是新對(duì)象
alert(a.x); // 20
alert(a.y); // 30

注意,ES5 提供了 Object.getPrototypeOf(O)方法,該方法直接返回對(duì)象的[[Prototype]]屬性——實(shí)例的初始原型。 然而,和__proto__相比,它只是 getter,它不允許 set 值。

var foo = {};
Object.getPrototypeOf(foo) == Object.prototype; // true

對(duì)象獨(dú)立于構(gòu)造函數(shù)

因?yàn)閷?shí)例的原型獨(dú)立于構(gòu)造函數(shù)和構(gòu)造函數(shù)的 prototype 屬性,構(gòu)造函數(shù)完成了自己的主要工作(創(chuàng)建對(duì)象)以后可以刪除。原型對(duì)象通過引用[[Prototype]]屬性繼續(xù)存在:

function A() {}
A.prototype.x = 10;  
var a = new A();
alert(a.x); // 10  
// 設(shè)置A為null - 顯示引用構(gòu)造函數(shù)
A = null;  
// 但如果.constructor屬性沒有改變的話,
// 依然可以通過它創(chuàng)建對(duì)象
var b = new a.constructor();
alert(b.x); // 10  
// 隱式的引用也刪除掉
delete a.constructor.prototype.constructor;
delete b.constructor.prototype.constructor;  
// 通過A的構(gòu)造函數(shù)再也不能創(chuàng)建對(duì)象了
// 但這2個(gè)對(duì)象依然有自己的原型
alert(a.x); // 10
alert(b.x); // 10

instanceof 操作符的特性

我們是通過構(gòu)造函數(shù)的 prototype 屬性來顯示引用原型的,這和 instanceof 操作符有關(guān)。該操作符是和原型鏈一起工作的,而不是構(gòu)造函數(shù),考慮到這一點(diǎn),當(dāng)檢測(cè)對(duì)象的時(shí)候往往會(huì)有誤解:

if (foo instanceof Foo) {
  ...
}

這不是用來檢測(cè)對(duì)象 foo 是否是用 Foo 構(gòu)造函數(shù)創(chuàng)建的,所有 instanceof 運(yùn)算符只需要一個(gè)對(duì)象屬性—— foo.[[Prototype]],在原型鏈中從 Foo.prototype 開始檢查其是否存在。instanceof 運(yùn)算符是通過構(gòu)造函數(shù)里的內(nèi)部方法[[HasInstance]]來激活的。

讓我們來看看這個(gè)例子:

function A() {}
A.prototype.x = 10;  
var a = new A();
alert(a.x); // 10  
alert(a instanceof A); // true  
// 如果設(shè)置原型為null
A.prototype = null;  
// ..."a"依然可以通過a.[[Prototype]]訪問原型
alert(a.x); // 10  
// 不過,instanceof操作符不能再正常使用了
// 因?yàn)樗菑臉?gòu)造函數(shù)的prototype屬性來實(shí)現(xiàn)的
alert(a instanceof A); // 錯(cuò)誤,A.prototype不是對(duì)象

另一方面,可以由構(gòu)造函數(shù)來創(chuàng)建對(duì)象,但如果對(duì)象的[[Prototype]]屬性和構(gòu)造函數(shù)的 prototype 屬性的值設(shè)置的是一樣的話,instanceof 檢查的時(shí)候會(huì)返回 true:

function B() {}
var b = new B();  
alert(b instanceof B); // true  
function C() {}  
var __proto = {
  constructor: C
};  
C.prototype = __proto;
b.__proto__ = __proto;  
alert(b instanceof C); // true
alert(b instanceof B); // false

原型可以存放方法并共享屬性

大部分程序里使用原型是用來存儲(chǔ)對(duì)象的方法、默認(rèn)狀態(tài)和共享對(duì)象的屬性。

事實(shí)上,對(duì)象可以擁有自己的狀態(tài) ,但方法通常是一樣的。 因此,為了內(nèi)存優(yōu)化,方法通常是在原型里定義的。 這意味著,這個(gè)構(gòu)造函數(shù)創(chuàng)建的所有實(shí)例都可以共享找個(gè)方法。

function A(x) {
  this.x = x || 100;
}   
A.prototype = (function () {  
  // 初始化上下文
  // 使用額外的對(duì)象  
  var _someSharedVar = 500;  
  function _someHelper() {
    alert('internal helper: ' + _someSharedVar);
  }  
  function method1() {
    alert('method1: ' + this.x);
  }  
  function method2() {
    alert('method2: ' + this.x);
    _someHelper();
  }  
  // 原型自身
  return {
    constructor: A,
    method1: method1,
    method2: method2
  };  
})();  
var a = new A(10);
var b = new A(20);  
a.method1(); // method1: 10
a.method2(); // method2: 10, internal helper: 500  
b.method1(); // method1: 20
b.method2(); // method2: 20, internal helper: 500  
// 2個(gè)對(duì)象使用的是原型里相同的方法
alert(a.method1 === b.method1); // true
alert(a.method2 === b.method2); // true

讀寫屬性

正如我們提到,讀取和寫入屬性值是通過內(nèi)部的[[Get]]和[[Put]]方法。這些內(nèi)部方法是通過屬性訪問器激活的:點(diǎn)標(biāo)記法或者索引標(biāo)記法:

// 寫入
foo.bar = 10; // 調(diào)用了[[Put]]  
console.log(foo.bar); // 10, 調(diào)用了[[Get]]
console.log(foo['bar']); // 效果一樣

讓我們用偽代碼來看一下這些方法是如何工作的:

[[Get]]方法
[[Get]]也會(huì)從原型鏈中查詢屬性,所以通過對(duì)象也可以訪問原型中的屬性。  
O.[[Get]](P):  
// 如果是自己的屬性,就返回
if (O.hasOwnProperty(P)) {
  return O.P;
}  
// 否則,繼續(xù)分析原型
var __proto = O.[[Prototype]];  
// 如果原型是null,返回undefined
// 這是可能的:最頂層Object.prototype.[[Prototype]]是null
if (__proto === null) {
  return undefined;
}  
// 否則,對(duì)原型鏈遞歸調(diào)用[[Get]],在各層的原型中查找屬性
// 直到原型為null
return __proto.[[Get]](P)

請(qǐng)注意,因?yàn)閇[Get]]在如下情況也會(huì)返回 undefined:

if (window.someObject) {
  ...
}

這里,在 window 里沒有找到 someObject 屬性,然后會(huì)在原型里找,原型的原型里找,以此類推,如果都找不到,按照定義就返回 undefined。

注意:in 操作符也可以負(fù)責(zé)查找屬性(也會(huì)查找原型鏈):

if ('someObject' in window) {
  ...
}

這有助于避免一些特殊問題:比如即便 someObject 存在,在 someObject 等于 false 的時(shí)候,第一輪檢測(cè)就通不過。

[[Put]]方法
[[Put]]方法可以創(chuàng)建、更新對(duì)象自身的屬性,并且掩蓋原型里的同名屬性  
O.[[Put]](P, V):  
// 如果不能給屬性寫值,就退出
if (!O.[[CanPut]](P)) {
  return;
}  
// 如果對(duì)象沒有自身的屬性,就創(chuàng)建它
// 所有的attributes特性都是false
if (!O.hasOwnProperty(P)) {
  createNewProperty(O, P, attributes: {
    ReadOnly: false,
    DontEnum: false,
    DontDelete: false,
    Internal: false
  });
}  
// 如果屬性存在就設(shè)置值,但不改變attributes特性
O.P = V  
return;

例如:

Object.prototype.x = 100;  
var foo = {};
console.log(foo.x); // 100, 繼承屬性  
foo.x = 10; // [[Put]]
console.log(foo.x); // 10, 自身屬性  
delete foo.x;
console.log(foo.x); // 重新是100,繼承屬性

請(qǐng)注意,不能掩蓋原型里的只讀屬性,賦值結(jié)果將忽略,這是由內(nèi)部方法[[CanPut]]控制的。

// 例如,屬性length是只讀的,我們來掩蓋一下length試試  
function SuperString() {
  /* nothing */
}  
SuperString.prototype = new String("abc");  
var foo = new SuperString();  
console.log(foo.length); // 3, "abc"的長(zhǎng)度 
// 嘗試掩蓋
foo.length = 5;
console.log(foo.length); // 依然是3

但在 ES5 的嚴(yán)格模式下,如果掩蓋只讀屬性的話,會(huì)保存 TypeError 錯(cuò)誤。

屬性訪問器

內(nèi)部方法[[Get]]和[[Put]]在 ECMAScript 里是通過點(diǎn)符號(hào)或者索引法來激活的,如果屬性標(biāo)示符是合法的名字的話,可以通過“.”來訪問,而索引方運(yùn)行動(dòng)態(tài)定義名稱。

var a = {testProperty: 10};  
alert(a.testProperty); // 10, 點(diǎn)
alert(a['testProperty']); // 10, 索引  
var propertyName = 'Property';
alert(a['test' + propertyName]); // 10, 動(dòng)態(tài)屬性通過索引的方式

這里有一個(gè)非常重要的特性——屬性訪問器總是使用 ToObject 規(guī)范來對(duì)待“.”左邊的值。這種隱式轉(zhuǎn)化和這句“在 JavaScript 中一切都是對(duì)象”有關(guān)系,(然而,當(dāng)我們已經(jīng)知道了,JavaScript 里不是所有的值都是對(duì)象)。

如果對(duì)原始值進(jìn)行屬性訪問器取值,訪問之前會(huì)先對(duì)原始值進(jìn)行對(duì)象包裝(包括原始值),然后通過包裝的對(duì)象進(jìn)行訪問屬性,屬性訪問以后,包裝對(duì)象就會(huì)被刪除。

例如:

var a = 10; // 原始值  
// 但是可以訪問方法(就像對(duì)象一樣)
alert(a.toString()); // "10"  
// 此外,我們可以在a上創(chuàng)建一個(gè)心屬性
a.test = 100; // 好像是沒問題的  
// 但,[[Get]]方法沒有返回該屬性的值,返回的卻是undefined
alert(a.test); // undefined

那么,為什么整個(gè)例子里的原始值可以訪問 toString 方法,而不能訪問新創(chuàng)建的 test 屬性呢?

答案很簡(jiǎn)單:

首先,正如我們所說,使用屬性訪問器以后,它已經(jīng)不是原始值了,而是一個(gè)包裝過的中間對(duì)象(整個(gè)例子是使用 new Number(a)),而 toString 方法這時(shí)候是通過原型鏈查找到的:

// 執(zhí)行a.toString()的原理:
1. wrapper = new Number(a);
2. wrapper.toString(); // "10"
3. delete wrapper;

接下來,[[Put]]方法創(chuàng)建新屬性時(shí)候,也是通過包裝裝的對(duì)象進(jìn)行的:

// 執(zhí)行a.test = 100的原理:
1. wrapper = new Number(a);
2. wrapper.test = 100;
3. delete wrapper;

我們看到,在第3步的時(shí)候,包裝的對(duì)象以及刪除了,隨著新創(chuàng)建的屬性頁(yè)被刪除了——?jiǎng)h除包裝對(duì)象本身。

然后使用[[Get]]獲取 test 值的時(shí)候,再一次創(chuàng)建了包裝對(duì)象,但這時(shí)候包裝的對(duì)象已經(jīng)沒有 test 屬性了,所以返回的是 undefined:

// 執(zhí)行a.test的原理:  
1. wrapper = new Number(a);
2. wrapper.test; // undefined

這種方式解釋了原始值的讀取方式,另外,任何原始值如果經(jīng)常用在訪問屬性的話,時(shí)間效率考慮,都是直接用一個(gè)對(duì)象替代它;與此相反,如果不經(jīng)常訪問,或者只是用于計(jì)算的話,到可以保留這種形式。

繼承

我們知道,ECMAScript 是使用基于原型的委托式繼承。鏈和原型在原型鏈里已經(jīng)提到過了。其實(shí),所有委托的實(shí)現(xiàn)和原型鏈的查找分析都濃縮到[[Get]]方法了。

如果你完全理解[[Get]]方法,那 JavaScript 中的繼承這個(gè)問題將不解自答了。

經(jīng)常在論壇上談?wù)?JavaScript 中的繼承時(shí),我都是用一行代碼來展示,事實(shí)上,我們不需要?jiǎng)?chuàng)建任何對(duì)象或函數(shù),因?yàn)樵撜Z(yǔ)言已經(jīng)是基于繼承的了,代碼如下:

alert(1..toString()); // "1"

我們已經(jīng)知道了[[Get]]方法和屬性訪問器的原理了,我們來看看都發(fā)生了什么:

  1. 首先,從原始值 1,通過 new Number(1)創(chuàng)建包裝對(duì)象
  2. 然后 toString 方法是從這個(gè)包裝對(duì)象上繼承得到的

為什么是繼承的? 因?yàn)樵?ECMAScript 中的對(duì)象可以有自己的屬性,包裝對(duì)象在這種情況下沒有 toString 方法。 因此它是從原理里繼承的,即 Number.prototype。

注意有個(gè)微妙的地方,在上面的例子中的兩個(gè)點(diǎn)不是一個(gè)錯(cuò)誤。第一點(diǎn)是代表小數(shù)部分,第二個(gè)才是一個(gè)屬性訪問器:

1.toString(); // 語(yǔ)法錯(cuò)誤!  
(1).toString(); // OK  
1..toString(); // OK  
1['toString'](); // OK

原型鏈

讓我們展示如何為用戶定義對(duì)象創(chuàng)建原型鏈,非常簡(jiǎn)單:

function A() {
  alert('A.[[Call]] activated');
  this.x = 10;
}
A.prototype.y = 20;  
var a = new A();
alert([a.x, a.y]); // 10 (自身), 20 (繼承)  
function B() {}  
// 最近的原型鏈方式就是設(shè)置對(duì)象的原型為另外一個(gè)新對(duì)象
B.prototype = new A();  
// 修復(fù)原型的constructor屬性,否則的話是A了 
B.prototype.constructor = B;  
var b = new B();
alert([b.x, b.y]); // 10, 20, 2個(gè)都是繼承的  
// [[Get]] b.x:
// b.x (no) -->
// b.[[Prototype]].x (yes) - 10  
// [[Get]] b.y
// b.y (no) -->
// b.[[Prototype]].y (no) -->
// b.[[Prototype]].[[Prototype]].y (yes) - 20  
// where b.[[Prototype]] === B.prototype,
// and b.[[Prototype]].[[Prototype]] === A.prototype

這種方法有兩個(gè)特性:

首先,B.prototype 將包含 x 屬性。乍一看這可能不對(duì),你可能會(huì)想 x 屬性是在 A 里定義的并且B構(gòu)造函數(shù)也是這樣期望的。盡管原型繼承正常情況是沒問題的,但 B 構(gòu)造函數(shù)有時(shí)候可能不需要 x 屬性,與基于 class 的繼承相比,所有的屬性都復(fù)制到后代子類里了。

盡管如此,如果有需要(模擬基于類的繼承)將x屬性賦給 B 構(gòu)造函數(shù)創(chuàng)建的對(duì)象上,有一些方法,我們后來來展示其中一種方式。

其次,這不是一個(gè)特征而是缺點(diǎn)——子類原型創(chuàng)建的時(shí)候,構(gòu)造函數(shù)的代碼也執(zhí)行了,我們可以看到消息"A.[[Call]] activated"顯示了兩次——當(dāng)用A構(gòu)造函數(shù)創(chuàng)建對(duì)象賦給 B.prototype 屬性的時(shí)候,另外一場(chǎng)是 a 對(duì)象創(chuàng)建自身的時(shí)候!

下面的例子比較關(guān)鍵,在父類的構(gòu)造函數(shù)拋出的異常:可能實(shí)際對(duì)象創(chuàng)建的時(shí)候需要檢查吧,但很明顯,同樣的 case,也就是就是使用這些父對(duì)象作為原型的時(shí)候就會(huì)出錯(cuò)。

function A(param) {
  if (!param) {
    throw 'Param required';
  }
  this.param = param;
}
A.prototype.x = 10;  
var a = new A(20);
alert([a.x, a.param]); // 10, 20  
function B() {}
B.prototype = new A(); // Error

此外,在父類的構(gòu)造函數(shù)有太多代碼的話也是一種缺點(diǎn)。

解決這些“功能”和問題,程序員使用原型鏈的標(biāo)準(zhǔn)模式(下面展示),主要目的就是在中間包裝構(gòu)造函數(shù)的創(chuàng)建,這些包裝構(gòu)造函數(shù)的鏈里包含需要的原型。

function A() {
  alert('A.[[Call]] activated');
  this.x = 10;
}
A.prototype.y = 20;  
var a = new A();
alert([a.x, a.y]); // 10 (自身), 20 (集成)  
function B() {
  // 或者使用A.apply(this, arguments)
  B.superproto.constructor.apply(this, arguments);
}  
// 繼承:通過空的中間構(gòu)造函數(shù)將原型連在一起
var F = function () {};
F.prototype = A.prototype; // 引用
B.prototype = new F();
B.superproto = A.prototype; // 顯示引用到另外一個(gè)原型上, "sugar"  
// 修復(fù)原型的constructor屬性,否則的就是A了
B.prototype.constructor = B;  
var b = new B();
alert([b.x, b.y]); // 10 (自身), 20 (集成)

注意,我們?cè)?b 實(shí)例上創(chuàng)建了自己的x屬性,通過 B.superproto.constructor 調(diào)用父構(gòu)造函數(shù)來引用新創(chuàng)建對(duì)象的上下文。

我們也修復(fù)了父構(gòu)造函數(shù)在創(chuàng)建子原型的時(shí)候不需要的調(diào)用,此時(shí),消息"A.[[Call]] activated"在需要的時(shí)候才會(huì)顯示。

為了在原型鏈里重復(fù)相同的行為(中間構(gòu)造函數(shù)創(chuàng)建,設(shè)置 superproto,恢復(fù)原始構(gòu)造函數(shù)),下面的模板可以封裝成一個(gè)非常方面的工具函數(shù),其目的是連接原型的時(shí)候不是根據(jù)構(gòu)造函數(shù)的實(shí)際名稱。

function inherit(child, parent) {
  var F = function () {};
  F.prototype = parent.prototype
  child.prototype = new F();
  child.prototype.constructor = child;
  child.superproto = parent.prototype;
  return child;
}

因此,繼承:

function A() {}
A.prototype.x = 10;  
function B() {}
inherit(B, A); // 連接原型  
var b = new B();
alert(b.x); // 10, 在A.prototype查找到

也有很多語(yǔ)法形式(包裝而成),但所有的語(yǔ)法行都是為了減少上述代碼里的行為。

例如,如果我們把中間的構(gòu)造函數(shù)放到外面,就可以優(yōu)化前面的代碼(因此,只有一個(gè)函數(shù)被創(chuàng)建),然后重用它:

var inherit = (function(){
  function F() {}
  return function (child, parent) {
    F.prototype = parent.prototype;
    child.prototype = new F;
    child.prototype.constructor = child;
    child.superproto = parent.prototype;
    return child;
  };
})();

由于對(duì)象的真實(shí)原型是[[Prototype]]屬性,這意味著 F.prototype 可以很容易修改和重用,因?yàn)橥ㄟ^ new F 創(chuàng)建的 child.prototype 可以從 child.prototype 的當(dāng)前值里獲取[[Prototype]]:

function A() {}
A.prototype.x = 10;  
function B() {}
inherit(B, A);  
B.prototype.y = 20;  
B.prototype.foo = function () {
  alert("B#foo");
};  
var b = new B();
alert(b.x); // 10, 在A.prototype里查到  
function C() {}
inherit(C, B);  
// 使用"superproto"語(yǔ)法糖
// 調(diào)用父原型的同名方法  
C.ptototype.foo = function () {
  C.superproto.foo.call(this);
  alert("C#foo");
};  
var c = new C();
alert([c.x, c.y]); // 10, 20  
c.foo(); // B#foo, C#foo

注意,ES5 為原型鏈標(biāo)準(zhǔn)化了這個(gè)工具函數(shù),那就是 Object.create 方法。ES3 可以使用以下方式實(shí)現(xiàn):

Object.create ||
Object.create = function (parent, properties) {
  function F() {}
  F.prototype = parent;
  var child = new F;
  for (var k in properties) {
    child[k] = properties[k].value;
  }
  return child;
}  
// 用法
var foo = {x: 10};
var bar = Object.create(foo, {y: {value: 20}});
console.log(bar.x, bar.y); // 10, 20

此外,所有模仿現(xiàn)在基于類的經(jīng)典繼承方式都是根據(jù)這個(gè)原則實(shí)現(xiàn)的,現(xiàn)在可以看到,它實(shí)際上不是基于類的繼承,而是連接原型的一個(gè)很方便的代碼重用。

結(jié)論

本章內(nèi)容已經(jīng)很充分和詳細(xì)了,希望這些資料對(duì)你有用,并且消除你對(duì) ECMAScript 的疑問,如果你有任何問題,請(qǐng)留言,我們一起討論。

其它參考

  1. Language Overview;
  2. Definitions;
  3. Regular Expression Literals;
  4. Types;
  5. Type Conversion;
  6. Array Initialiser;
  7. Object Initialiser;
  8. The new Operator;
  9. [[Call]];
  10. [[Construct]];
  11. Native ECMAScript Objects.