本文是系列(二)樟遣,記錄的是《你不知道的JavaScript》上卷第二部分的1-3章燃箭,第一篇連接http://www.reibang.com/p/23815e221ffe
本文目錄
- 第二部分 this和對(duì)象原型
- 第1章 關(guān)于this
- 第2章 this全面解析
- 第3章 對(duì)象
第二部分 this和對(duì)象原型
第1章 關(guān)于this
誤解:函數(shù)中的this指向函數(shù)自身
看下面的代碼
function foo(num) {
console.log("foo: " + num); // 記錄 foo 被調(diào)用的次數(shù)
this.count++;
}
foo.count = 0;
var i;
for (i = 0; i < 10; i++) {
if (i > 5) {
foo(i);
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被調(diào)用了多少次府瞄?
console.log(foo.count); // 0
console.log 語(yǔ)句產(chǎn)生了 4 條輸出敷矫,證明 foo(..) 確實(shí)被調(diào)用了 4 次础芍,但是 foo.count 仍然 是 0灸叼。顯然this并不指向函數(shù)自身神汹。
解決方法之一是使用 foo 標(biāo)識(shí)符替代 this 來(lái)引用函數(shù) 對(duì)象:
function foo(num) {
console.log("foo: " + num);
// 記錄 foo 被調(diào)用的次數(shù) foo.count++;
}
foo.count = 0
var i;
for (i = 0; i < 10; i++) {
if (i > 5) {
foo(i);
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被調(diào)用了多少次?
console.log(foo.count); // 4
從某種角度來(lái)說(shuō)這個(gè)方法確實(shí)“解決”了問(wèn)題古今,但可惜它忽略了真正的問(wèn)題——無(wú)法理解 this 的含義和工作原理——而是返回舒適區(qū)屁魏,且完全依賴于變量 foo 的詞法作用域。
另一種方法是強(qiáng)制 this 指向 foo 函數(shù)對(duì)象:
function foo(num) {
console.log("foo: " + num);
// 記錄 foo 被調(diào)用的次數(shù)
// 注意捉腥,在當(dāng)前的調(diào)用方式下(參見(jiàn)下方代碼)氓拼,this 確實(shí)指向 foo
this.count++;
}
foo.count = 0;
var i;
for (i = 0; i < 10; i++) {
if (i > 5) {
// 使用 call(..) 可以確保 this 指向函數(shù)對(duì)象 foo 本身
foo.call(foo, i);
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被調(diào)用了多少次?
console.log(foo.count); // 4
這次我們接受了 this抵碟,沒(méi)有回避它桃漾。如果你仍然感到困惑的話,不用擔(dān)心立磁,之后我們會(huì)詳 細(xì)解釋具體的原理呈队。
下面這段代碼非常的經(jīng)典
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log(this.a);
}
foo(); // ReferenceError: a is not defined
這段代碼的錯(cuò)誤不止一個(gè),非常完美(同時(shí)也令人傷感) 地展示了 this 多么容易誤導(dǎo)人唱歧。
之前我們說(shuō)過(guò) this 是在運(yùn)行時(shí)進(jìn)行綁定的宪摧,并不是在編寫(xiě)時(shí)綁定粒竖,它的上下文取決于函數(shù)調(diào) 用時(shí)的各種條件。this 的綁定和函數(shù)聲明的位置沒(méi)有任何關(guān)系几于,只取決于函數(shù)的調(diào)用方式蕊苗。
第2章 this全面解析
通常來(lái)說(shuō),尋找調(diào)用位置就是尋找“函數(shù)被調(diào)用的位置”沿彭,但是做起來(lái)并沒(méi)有這么簡(jiǎn)單朽砰, 因?yàn)槟承┚幊棠J娇赡軙?huì)隱藏真正的調(diào)用位置。
思考下面的代碼:
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函數(shù)別名喉刘!
var a = "oops, global"; // a 是全局對(duì)象的屬性
bar(); // "oops, global"
雖然 bar 是 obj.foo 的一個(gè)引用瞧柔,但是實(shí)際上,它引用的是 foo 函數(shù)本身睦裳,因此此時(shí)的 bar() 其實(shí)是一個(gè)不帶任何修飾的函數(shù)調(diào)用造锅,因此應(yīng)用了默認(rèn)綁定。
一種更微妙廉邑、更常見(jiàn)并且更出乎意料的情況發(fā)生在傳入回調(diào)函數(shù)時(shí):
function foo() {
console.log(this.a);
}
function doFoo(fn) {
// fn 其實(shí)引用的是 foo
fn(); // <-- 調(diào)用位置哥蔚!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局對(duì)象的屬性
doFoo(obj.foo); // "oops, global"
參數(shù)傳遞其實(shí)就是一種隱式賦值,因此我們傳入函數(shù)時(shí)也會(huì)被隱式賦值蛛蒙,所以結(jié)果和上一 個(gè)例子一樣糙箍。
如果把函數(shù)傳入語(yǔ)言內(nèi)置的函數(shù)而不是傳入你自己聲明的函數(shù),會(huì)發(fā)生什么呢牵祟?結(jié)果是一 樣的深夯,沒(méi)有區(qū)別:
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局對(duì)象的屬性
setTimeout(obj.foo, 100); // "oops, global"
JavaScript 環(huán)境中內(nèi)置的 setTimeout() 函數(shù)實(shí)現(xiàn)和下面的偽代碼類(lèi)似:
function setTimeout(fn, delay) {
// 等待 delay 毫秒 fn(); // <-- 調(diào)用位置!
}
第三方庫(kù)的許多函數(shù)诺苹,以及 JavaScript 語(yǔ)言和宿主環(huán)境中許多新的內(nèi)置函數(shù)塌西,都提供了一 個(gè)可選的參數(shù),通常被稱為“上下文”(context)筝尾,其作用和 bind(..) 一樣,確保你的回調(diào) 函數(shù)使用指定的 this办桨。
舉例來(lái)說(shuō):
function foo(el) {
console.log(el, this.id);
}
var obj = {
id: "awesome"
}; // 調(diào)用 foo(..) 時(shí)把 this 綁定到 obj
[1, 2, 3].forEach(foo, obj); // 1 awesome 2 awesome 3 awesome
這些函數(shù)實(shí)際上就是通過(guò) call(..) 或者 apply(..) 實(shí)現(xiàn)了顯式綁定筹淫,這樣你可以少些一些 代碼。如果我們使用forEach不傳第二個(gè)可選參數(shù)呢撞,結(jié)果會(huì)是1 undefined 2 undefined 3 undefined
在 JavaScript 中损姜,構(gòu)造函數(shù)只是一些 使用 new 操作符時(shí)被調(diào)用的函數(shù)。它們并不會(huì)屬于某個(gè)類(lèi)殊霞,也不會(huì)實(shí)例化一個(gè)類(lèi)摧阅。實(shí)際上, 它們甚至都不能說(shuō)是一種特殊的函數(shù)類(lèi)型绷蹲,它們只是被 new 操作符調(diào)用的普通函數(shù)而已棒卷。
使用 new 來(lái)調(diào)用函數(shù)顾孽,或者說(shuō)發(fā)生構(gòu)造函數(shù)調(diào)用時(shí),會(huì)自動(dòng)執(zhí)行下面的操作比规。
- 創(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ì)象。
判斷this
現(xiàn)在我們可以根據(jù)優(yōu)先級(jí)來(lái)判斷函數(shù)在某個(gè)調(diào)用位置應(yīng)用的是哪條規(guī)則灾常■耄可以按照下面的 順序來(lái)進(jìn)行判斷:
- 函數(shù)是否在 new 中調(diào)用(new 綁定)?如果是的話 this 綁定的是新創(chuàng)建的對(duì)象钞瀑。 var bar = new foo()
- 函數(shù)是否通過(guò) call沈撞、apply(顯式綁定)或者硬綁定調(diào)用?如果是的話仔戈,this 綁定的是 指定的對(duì)象关串。 var bar = foo.call(obj2) - - 3. 函數(shù)是否在某個(gè)上下文對(duì)象中調(diào)用(隱式綁定)?如果是的話监徘,this 綁定的是那個(gè)上 下文對(duì)象晋修。 var bar = obj1.foo()
- 如果都不是的話,使用默認(rèn)綁定凰盔。如果在嚴(yán)格模式下墓卦,就綁定到 undefined,否則綁定到 全局對(duì)象户敬。 var bar = foo()
就是這樣落剪。對(duì)于正常的函數(shù)調(diào)用來(lái)說(shuō),理解了這些知識(shí)你就可以明白 this 的綁定原理了尿庐。 不過(guò)……凡事總有例外忠怖。
如果你把 null 或者 undefined 作為 this 的綁定對(duì)象傳入 call、apply 或者 bind抄瑟,這些值 在調(diào)用時(shí)會(huì)被忽略凡泣,實(shí)際應(yīng)用的是默認(rèn)綁定規(guī)則:
function foo() {
console.log(this.a);
}
var a = 2;
foo.call(null); // 2
那么什么情況下你會(huì)傳入 null 呢?
一種非常常見(jiàn)的做法是使用 apply(..) 來(lái)“展開(kāi)”一個(gè)數(shù)組皮假,并當(dāng)作參數(shù)傳入一個(gè)函數(shù)鞋拟。 類(lèi)似地,bind(..) 可以對(duì)參數(shù)進(jìn)行柯里化(預(yù)先設(shè)置一些參數(shù))惹资,這種方法有時(shí)非常有用:
function foo(a, b) {
console.log("a:" + a + ", b:" + b);
}
// 把數(shù)組“展開(kāi)”成參數(shù)
foo.apply(null, [2, 3]); // a:2, b:3
// 使用 bind(..) 進(jìn)行柯里化
var bar = foo.bind(null, 2);
bar(3); // a:2, b:3
這兩種方法都需要傳入一個(gè)參數(shù)當(dāng)作 this 的綁定對(duì)象贺纲。如果函數(shù)并不關(guān)心 this 的話,你 仍然需要傳入一個(gè)占位值褪测,這時(shí) null 可能是一個(gè)不錯(cuò)的選擇猴誊,就像代碼所示的那樣潦刃。
在 ES6 中,可以用 ... 操作符代替 apply(..) 來(lái)“展 開(kāi)”數(shù)組稠肘,foo(...[1,2]) 和 foo(1,2) 是一樣的福铅,這樣可以避免不必要的 this 綁定∠钜酰可惜滑黔,在 ES6 中沒(méi)有柯里化的相關(guān)語(yǔ)法,因此還是需要使用 bind(..)环揽。
然而略荡,總是使用 null 來(lái)忽略 this 綁定可能產(chǎn)生一些副作用。如果某個(gè)函數(shù)確實(shí)使用了 this(比如第三方庫(kù)中的一個(gè)函數(shù))歉胶,那默認(rèn)綁定規(guī)則會(huì)把 this 綁定到全局對(duì)象(在瀏覽 器中這個(gè)對(duì)象是 window)汛兜,這將導(dǎo)致不可預(yù)計(jì)的后果(比如修改全局對(duì)象)。
一種“更安全”的做法是傳入一個(gè)特殊的對(duì)象通今,把 this 綁定到這個(gè)對(duì)象不會(huì)對(duì)你的程序 產(chǎn)生任何副作用粥谬。就像網(wǎng)絡(luò)(以及軍隊(duì))一樣,我們可以創(chuàng)建一個(gè)“DMZ”(demilitarized zone辫塌,非軍事區(qū))對(duì)象——它就是一個(gè)空的非委托的對(duì)象漏策。
如果我們?cè)诤雎?this 綁定時(shí)總是傳入一個(gè) DMZ 對(duì)象,那就什么都不用擔(dān)心了臼氨,因?yàn)槿魏?對(duì)于 this 的使用都會(huì)被限制在這個(gè)空對(duì)象中掺喻,不會(huì)對(duì)全局對(duì)象產(chǎn)生任何影響。
無(wú)論你叫它什么储矩,在 JavaScript 中創(chuàng)建一個(gè)空對(duì)象最簡(jiǎn)單的方法都是 Object.create(null) ( 詳 細(xì) 介 紹 請(qǐng) 看 第 5 章 )感耙。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
使用變量名 ? 不僅讓函數(shù)變得更加“安全”即硼,而且可以提高代碼的可讀性,因?yàn)?? 表示 “我希望 this 是空”屡拨,這比 null 的含義更清楚谦絮。不過(guò)再說(shuō)一遍,你可以用任何喜歡的名字 來(lái)命名 DMZ 對(duì)象洁仗。
另一個(gè)需要注意的是,你有可能(有意或者無(wú)意地)創(chuàng)建一個(gè)函數(shù)的“間接引用”性锭,在這 種情況下赠潦,調(diào)用這個(gè)函數(shù)會(huì)應(yīng)用默認(rèn)綁定規(guī)則。 間接引用最容易在賦值時(shí)發(fā)生:
function foo() {
console.log(this.a);
}
var a = 2;
var o = {
a: 3,
foo: foo
};
var p = {
a: 4
};
o.foo(); // 3
(p.foo = o.foo)(); // 2
賦值表達(dá)式 p.foo = o.foo 的返回值是目標(biāo)函數(shù)的引用草冈,因此調(diào)用位置是 foo() 而不是 p.foo() 或者 o.foo()她奥。根據(jù)我們之前說(shuō)過(guò)的瓮增,這里會(huì)應(yīng)用默認(rèn)綁定。 注意:對(duì)于默認(rèn)綁定來(lái)說(shuō)哩俭,決定 this 綁定對(duì)象的并不是調(diào)用位置是否處于嚴(yán)格模式绷跑,而是 函數(shù)體是否處于嚴(yán)格模式。如果函數(shù)體處于嚴(yán)格模式凡资,this 會(huì)被綁定到 undefined砸捏,否則 this 會(huì)被綁定到全局對(duì)象。
我們來(lái)看看箭頭函數(shù)的詞法作用域:
function foo() { // 返回一個(gè)箭頭函數(shù)
return (a) => { //this 繼承自 foo()
console.log(this.a);
};
}
var obj1 = {
a: 2
};
var obj2 = {
a: 3
};
var bar = foo.call(obj1);
bar.call(obj2); // 2, 不是 3 隙赁!
foo() 內(nèi)部創(chuàng)建的箭頭函數(shù)會(huì)捕獲調(diào)用時(shí) foo() 的 this垦藏。由于 foo() 的 this 綁定到 obj1, bar(引用箭頭函數(shù))的 this 也會(huì)綁定到 obj1伞访,箭頭函數(shù)的綁定無(wú)法被修改掂骏。(new 也不 行!)
箭頭函數(shù)最常用于回調(diào)函數(shù)中厚掷,例如事件處理器或者定時(shí)器:
function foo() {
setTimeout(() => {
// 這里的 this 在此法上繼承自 foo()
console.log(this.a);
}, 100);
}
var obj = {
a: 2
};
foo.call(obj); // 2
第3章 對(duì)象
有一種常見(jiàn)的錯(cuò)誤說(shuō)法是“JavaScript 中萬(wàn)物皆是對(duì)象”弟灼,這顯然是錯(cuò)誤的。
實(shí)際上冒黑,JavaScript 中有許多特殊的對(duì)象子類(lèi)型田绑,我們可以稱之為復(fù)雜基本類(lèi)型。
函數(shù)就是對(duì)象的一個(gè)子類(lèi)型(從技術(shù)角度來(lái)說(shuō)就是“可調(diào)用的對(duì)象”)薛闪。JavaScript 中的函 數(shù)是“一等公民”辛馆,因?yàn)樗鼈儽举|(zhì)上和普通的對(duì)象一樣(只是可以調(diào)用),所以可以像操作 其他對(duì)象一樣操作函數(shù)(比如當(dāng)作另一個(gè)函數(shù)的參數(shù))豁延。
數(shù)組也是對(duì)象的一種類(lèi)型昙篙,具備一些額外的行為。數(shù)組中內(nèi)容的組織方式比一般的對(duì)象要 稍微復(fù)雜一些诱咏。
思考下面的代碼:
var myObject = { a: 2 };
myObject.a; // 2
myObject["a"]; // 2
如果要訪問(wèn) myObject 中 a 位置上的值苔可,我們需要使用 . 操作符或者 [] 操作符。.a 語(yǔ)法通 常被稱為“屬性訪問(wèn)”袋狞,["a"] 語(yǔ)法通常被稱為“鍵訪問(wèn)”焚辅。
這兩種語(yǔ)法的主要區(qū)別在于 . 操作符要求屬性名滿足標(biāo)識(shí)符的命名規(guī)范,而 [".."] 語(yǔ)法 可以接受任意 UTF-8/Unicode 字符串作為屬性名苟鸯。舉例來(lái)說(shuō)同蜻,如果要引用名稱為 "Super- Fun!" 的屬性,那就必須使用 ["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)換成字符串
數(shù)組也是對(duì)象桃序,所以雖然每個(gè)下標(biāo)都是整數(shù)杖虾,你仍然可以給數(shù)組添加屬性:
var myArray = ["foo", 42, "bar"];
myArray.baz = "baz";
console.log(myArray.length); // 3
console.log(myArray.baz); // "baz"
console.log(myArray) //["foo", 42, "bar", baz: "baz"]
可以看到雖然添加了命名屬性(無(wú)論是通過(guò) . 語(yǔ)法還是 [] 語(yǔ)法),數(shù)組的 length 值并未發(fā) 生變化媒熊。
你完全可以把數(shù)組當(dāng)作一個(gè)普通的鍵 / 值對(duì)象來(lái)使用奇适,并且不添加任何數(shù)值索引,但是這 并不是一個(gè)好主意泛释。數(shù)組和普通的對(duì)象都根據(jù)其對(duì)應(yīng)的行為和用途進(jìn)行了優(yōu)化滤愕,所以最好 只用對(duì)象來(lái)存儲(chǔ)鍵 / 值對(duì),只用數(shù)組來(lái)存儲(chǔ)數(shù)值下標(biāo) / 值對(duì)怜校。
注意:如果你試圖向數(shù)組添加一個(gè)屬性间影,但是屬性名“看起來(lái)”像一個(gè)數(shù)字,那它會(huì)變成 一個(gè)數(shù)值下標(biāo)(因此會(huì)修改數(shù)組的內(nèi)容而不是添加一個(gè)屬性):
var myArray = ["foo", 42, "bar"];
myArray["3"] = "baz";
myArray.length; // 4
myArray[3]; // "baz"
在 ES5 之前茄茁,JavaScript 語(yǔ)言本身并沒(méi)有提供可以直接檢測(cè)屬性特性的方法魂贬,比如判斷屬 性是否是只讀。
var myObject = {
a: 2
};
Object.getOwnPropertyDescriptor(myObject, "a");
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
如你所見(jiàn)裙顽,這個(gè)普通的對(duì)象屬性對(duì)應(yīng)的屬性描述符(也被稱為“數(shù)據(jù)描述符”付燥,因?yàn)樗?只保存一個(gè)數(shù)據(jù)值)可不僅僅只是一個(gè) 2。它還包含另外三個(gè)特性:writable(可寫(xiě))愈犹、 enumerable(可枚舉)和 configurable(可配置)键科。
在創(chuàng)建普通屬性時(shí)屬性描述符會(huì)使用默認(rèn)值,我們也可以使用 Object.defineProperty(..) 來(lái)添加一個(gè)新屬性或者修改一個(gè)已有屬性(如果它是 configurable)并對(duì)特性進(jìn)行設(shè)置漩怎。
舉例來(lái)說(shuō):
var myObject = {};
Object.defineProperty(myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true
});
myObject.a; // 2
我們使用 defineProperty(..) 給 myObject 添加了一個(gè)普通的屬性并顯式指定了一些特性勋颖。 然而,一般來(lái)說(shuō)你不會(huì)使用這種方式勋锤,除非你想修改屬性描述符饭玲。
注意:即便屬性是 configurable:false,我們還是可以 把 writable 的狀態(tài)由 true 改為 false叁执,但是無(wú)法由 false 改為 true茄厘。除了無(wú)法修改,configurable:false 還會(huì)禁止刪除這個(gè)屬性谈宛。
不可變性
在 JavaScript 程序中很少需要深不可變性次哈,但是我們需要做一下了解:
1. 對(duì)象常量
結(jié)合 writable:false 和 configurable:false 就可以創(chuàng)建一個(gè)真正的常量屬性(不可修改、 重定義或者刪除)
2. 禁止擴(kuò)展
如 果 你 想 禁 止 一 個(gè) 對(duì) 象 添 加 新 屬 性 并 且 保 留 已 有 屬 性吆录, 可 以 使 用 Object.prevent Extensions(..):
var myObject = {
a: 2
};
Object.preventExtensions(myObject);
myObject.b = 3;
console.log(myObject.b); // undefined
3. 密封
Object.seal(..) 會(huì)創(chuàng)建一個(gè)“密封”的對(duì)象亿乳,這個(gè)方法實(shí)際上會(huì)在一個(gè)現(xiàn)有對(duì)象上調(diào)用 Object.preventExtensions(..) 并把所有現(xiàn)有屬性標(biāo)記為 configurable:false。(不可以修改屬性,但是可以修改值)
4. 凍結(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ú)法修改它們 的值滋恬。(不可以修改值聊训,但是可以修改屬性)
思考下面的代碼:
var myObject = { // 給 a 定義一個(gè) getter
get a() {
return 2;
}
};
Object.defineProperty(
myObject, // 目標(biāo)對(duì)象
"b", // 屬性名
{
// 描述符
// 給 b 設(shè)置一個(gè) getter
get: function () {
return this.a * 2
},
// 確保 b 會(huì)出現(xiàn)在對(duì)象的屬性列表中
enumerable: true
});
console.log(myObject.a); // 2
console.log(myObject.b); // 4
不管是對(duì)象文字語(yǔ)法中的 get a() { .. },還是 defineProperty(..) 中的顯式定義恢氯,二者 都會(huì)在對(duì)象中創(chuàng)建一個(gè)不包含值的屬性带斑,對(duì)于這個(gè)屬性的訪問(wèn)會(huì)自動(dòng)調(diào)用一個(gè)隱藏函數(shù), 它的返回值會(huì)被當(dāng)作屬性訪問(wèn)的返回值:
var myObject = {
// 給 a 定義一個(gè) getter
get a() {
return 2;
}
};
myObject.a = 3;
console.log(myObject.a); // 2
由于我們只定義了 a 的 getter勋拟,所以對(duì) a 的值進(jìn)行設(shè)置時(shí) set 操作會(huì)忽略賦值操作勋磕,不會(huì)拋 出錯(cuò)誤。而且即便有合法的 setter敢靡,由于我們自定義的 getter 只會(huì)返回 2挂滓,所以 set 操作是 沒(méi)有意義的。
為了讓屬性更合理啸胧,定義getter的同時(shí)還應(yīng)當(dāng)定義 setter赶站。通常來(lái)說(shuō) getter 和 setter 是成對(duì)出現(xiàn)的(只定義一個(gè)的話 通常會(huì)產(chǎn)生意料之外的行為)
存在性
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]] 鏈烟逊。
看起來(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。
之前介紹 enumerable 屬性描述符特性時(shí)我們簡(jiǎn)單解釋過(guò)什么是“可枚舉性”冬阳,現(xiàn)在詳細(xì)介紹一下:
var myObject = {};
Object.defineProperty(myObject, "a",
// 讓 a 像普通屬性一樣可以枚舉
{
enumerable: true,
value: 2
});
Object.defineProperty(myObject, "b",
// 讓 b 不可枚舉
{
enumerable: false,
value: 3
});
console.log(myObject.b); // 3
console.log("b" in myObject); // true
console.log(myObject.hasOwnProperty("b")); // true // .......
for (var k in myObject) {
console.log(k, myObject[k]);
} // "a" 2
console.log(Object.keys(myObject)) //["a"]
console.log(Object.getOwnPropertyNames(myObject)) //["a", "b"]
可以看到蛤虐,myObject.b 確實(shí)存在并且有訪問(wèn)值,但是卻不會(huì)出現(xiàn)在 for..in 循環(huán)中(盡管 可以通過(guò) in 操作符來(lái)判斷是否存在)肝陪。原因是“可枚舉”就相當(dāng)于“可以出現(xiàn)在對(duì)象屬性 的遍歷中”驳庭。
Object.keys(..) 會(huì)返回一個(gè)數(shù)組,包含所有可枚舉屬性,Object.getOwnPropertyNames(..) 會(huì)返回一個(gè)數(shù)組饲常,包含所有屬性蹲堂,無(wú)論它們是否可枚舉。
注意:
在數(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ù)值索引朽基。
使用 for..in 遍歷對(duì)象是無(wú)法直接獲取屬性值的,因?yàn)樗鼘?shí)際上遍歷的是對(duì)象中的所有可 枚舉屬性离陶,你需要手動(dòng)獲取屬性值稼虎。
那么如何直接遍歷值而不是數(shù)組下標(biāo)(或者對(duì)象屬性)呢?幸好招刨,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ù)組不同计济,普通的對(duì)象沒(méi)有內(nèi)置的 @@iterator茸苇,所以無(wú)法自動(dòng)完成 for..of 遍歷。之所 以要這樣做沦寂,有許多非常復(fù)雜的原因学密,不過(guò)簡(jiǎn)單來(lái)說(shuō),這樣做是為了避免影響未來(lái)的對(duì)象 類(lèi)型传藏。
當(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
小結(jié):
屬性不一定包含值——它們可能是具備 getter/setter 的“訪問(wèn)描述符”毯侦。此外哭靖,屬性可以是 可枚舉或者不可枚舉的,這決定了它們是否會(huì)出現(xiàn)在 for..in 循環(huán)中侈离。
該看121頁(yè)了 3.4遍歷