欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

深入理解JavaScript系列(18):面向?qū)ο缶幊讨瓻CMAScript實(shí)現(xiàn)

 更新時(shí)間:2015年03月05日 09:13:25   投稿:junjie  
這篇文章主要介紹了深入理解JavaScript系列(18):面向?qū)ο缶幊讨瓻CMAScript實(shí)現(xiàn),本文講解了數(shù)據(jù)類型、原始值類型、Object類型、動(dòng)態(tài)性、內(nèi)置對(duì)象、原生對(duì)象及宿主對(duì)象等內(nèi)容,需要的朋友可以參考下

介紹

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

英文原文:http://dmitrysoshnikov.com/ecmascript/chapter-7-2-oop-ecmascript-implementation/
注:由于篇幅太長(zhǎng)了,難免出現(xiàn)錯(cuò)誤,時(shí)刻保持修正中。

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

復(fù)制代碼 代碼如下:

ECMAScript is an object-oriented programming language supporting delegating inheritance based on prototypes.

ECMAScript是一種面向?qū)ο笳Z言,支持基于原型的委托式繼承。
我們將從最基本的數(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)弱類型語言,它還是有數(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語句的。

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

復(fù)制代碼 代碼如下:

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)閠ypeof(Boolean)的結(jié)果是function,所以函數(shù)Boolean、String、Number是有原型的(下面的讀寫屬性章節(jié)也會(huì)提到)。

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

復(fù)制代碼 代碼如下:

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ù)類型。

Object is an unordered collection of key-value pairs.
對(duì)象是一個(gè)包含key-value對(duì)的無序集合

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

例如:

復(fù)制代碼 代碼如下:

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

例如:

復(fù)制代碼 代碼如下:

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ò)展新的屬性,并且它的屬性頁不能刪除或者修改。他們是所謂的凍結(jié)對(duì)象,可以通過應(yīng)用Object.freeze(o)方法得到。

復(fù)制代碼 代碼如下:

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)方法來定義屬性:

復(fù)制代碼 代碼如下:

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ī)范的語義。從這點(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)換省原始值,反之亦然。

復(fù)制代碼 代碼如下:

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á)式初始化器:

復(fù)制代碼 代碼如下:

// 等價(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)語義就是按照新賦值的類型來使用,例如在當(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)字面量的語義在類型改變以后卻不一定改變。

復(fù)制代碼 代碼如下:

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,字面量的語義是不被改變的。 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á)式的語義都是等價(jià)的,regexp字面量只在一句里存在,并且再解析階段創(chuàng)建,但RegExp構(gòu)造器創(chuàng)建的卻是新對(duì)象,所以這可能會(huì)導(dǎo)致出一些問題,如lastIndex的值在測(cè)試的時(shí)候結(jié)果是錯(cuò)誤的:

復(fù)制代碼 代碼如下:

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

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

復(fù)制代碼 代碼如下:

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"的概念在這里也是不正確的:

復(fù)制代碼 代碼如下:

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ì)象可以稱之為哈希表:

復(fù)制代碼 代碼如下:

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

此外,一些屬性有特定的getter / setter方法​​,所以也可能導(dǎo)致混淆這個(gè)概念:
復(fù)制代碼 代碼如下:

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

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

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

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

復(fù)制代碼 代碼如下:

a = {}
a.class # Hash
 
a.length # 0
 
# new "key-value" pair
a['length'] = 10;
 
# 語義上,用點(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)并沒有定義“哈?!保ㄒ约邦愃疲┑母拍?。但是,有這樣的結(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)用:

復(fù)制代碼 代碼如下:

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ì)象參與各種操作,例如:
復(fù)制代碼 代碼如下:

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í)間:

復(fù)制代碼 代碼如下:

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)使用的:
復(fù)制代碼 代碼如下:

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)鍵字也可以):

復(fù)制代碼 代碼如下:

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é)果的:

復(fù)制代碼 代碼如下:

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)化:
復(fù)制代碼 代碼如下:

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或類似的方法來管理這些屬性。

復(fù)制代碼 代碼如下:

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]] + "]" 。例如:

復(fù)制代碼 代碼如下:

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"。
復(fù)制代碼 代碼如下:

// 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ì)象):

復(fù)制代碼 代碼如下:

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ù)制代碼 代碼如下:

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中的效果:

復(fù)制代碼 代碼如下:

// 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)也可以省略:

復(fù)制代碼 代碼如下:

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ù)(初始化階段)里的shis被設(shè)置為新創(chuàng)建的對(duì)象 。

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

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

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

復(fù)制代碼 代碼如下:

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é)果:

復(fù)制代碼 代碼如下:

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ù)自身的重要引用:

復(fù)制代碼 代碼如下:

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ì)象的引用:

復(fù)制代碼 代碼如下:

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屬性:

復(fù)制代碼 代碼如下:

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

因此,對(duì)函數(shù)的原型引用需要手工恢復(fù):
復(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)語句不支持了,不過第5版規(guī)范里,通過[[Enumerable]] 特性提供了控制可枚舉狀態(tài)enumerable的能力。

復(fù)制代碼 代碼如下:

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)不能被再被修改了)。

復(fù)制代碼 代碼如下:

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

例如:

復(fù)制代碼 代碼如下:

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ì)象的原型:

復(fù)制代碼 代碼如下:

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值。
復(fù)制代碼 代碼如下:

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ù)存在:

復(fù)制代碼 代碼如下:

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ì)有誤解:

復(fù)制代碼 代碼如下:

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è)例子:

復(fù)制代碼 代碼如下:

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:

復(fù)制代碼 代碼如下:

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è)方法。

復(fù)制代碼 代碼如下:

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)記法:

復(fù)制代碼 代碼如下:

// 寫入
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):

復(fù)制代碼 代碼如下:

// 如果是自己的屬性,就返回
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:
復(fù)制代碼 代碼如下:

if (window.someObject) {
  ...
}

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

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

復(fù)制代碼 代碼如下:

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

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

[[Put]]方法

[[Put]]方法可以創(chuàng)建、更新對(duì)象自身的屬性,并且掩蓋原型里的同名屬性。

O.[[Put]](P, V):

復(fù)制代碼 代碼如下:

// 如果不能給屬性寫值,就退出
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;

例如:
復(fù)制代碼 代碼如下:

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)定義名稱。

復(fù)制代碼 代碼如下:

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ì)被刪除。

例如:

復(fù)制代碼 代碼如下:

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í)候是通過原型鏈查找到的:

復(fù)制代碼 代碼如下:

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

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

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

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

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

復(fù)制代碼 代碼如下:

// 執(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ù)揓avaScript中的繼承時(shí),我都是用一行代碼來展示,事實(shí)上,我們不需要?jiǎng)?chuàng)建任何對(duì)象或函數(shù),因?yàn)樵撜Z言已經(jīng)是基于繼承的了,代碼如下:

復(fù)制代碼 代碼如下:

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

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

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

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

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

復(fù)制代碼 代碼如下:

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

原型鏈

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

復(fù)制代碼 代碼如下:

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ò)。

復(fù)制代碼 代碼如下:

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ù)的鏈里包含需要的原型。

復(fù)制代碼 代碼如下:

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è)赽實(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í)際名稱。

復(fù)制代碼 代碼如下:

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;
}

因此,繼承:

復(fù)制代碼 代碼如下:

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

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

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

復(fù)制代碼 代碼如下:

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]]:

復(fù)制代碼 代碼如下:

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"語法糖
// 調(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):

復(fù)制代碼 代碼如下:

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)留言,我們一起討論。

相關(guān)文章

最新評(píng)論