javascript
你所不知道的 JavaScript
目錄
- 一、作用域和閉包
- 1. 附錄B 塊作用域的替代方案
- 1.1 Traceur - 將ES6 代碼生成兼容ES5的工具
- 1.2 隱式和顯式作用域
- 2. 附錄C this 詞法
- 二、this 和對象原型
- 1. 關于 this
- 1.1 關于的錯誤認識
- 1.2 this 是什么
- 2. this 的全面解析
- 2.1 調用位置
- 3. 對象
- 3.1 語法
- 3.2 類型
- 3.3 對象內容 - 屬性
- ~ 未完待續~
一、作用域和閉包
1. 附錄B 塊作用域的替代方案
1.1 Traceur - 將ES6 代碼生成兼容ES5的工具
Google 維護著一個名為 Traceur 的項目,該項目正是用來將ES6 代碼轉換成兼容 ES6 之前的環境(大部分是ES5,但不是全部)。TC39 委員會依賴這個工具(也有其他工具)來測試他們指定的語義化相關的功能。
Traceur 會將我們的代碼片段轉換成的樣子:
{try {throw undefined;} catch (a) {a = 2;console.log( a );} } console.log( a );1.2 隱式和顯式作用域
let 作用域:
let (a = 2) {console.log( a ); // 2 } console.log( a ); // ReferenceErrorlet 聲明會創建一個顯示的作用域并與其進行綁定。
但是這里有一個小問題,let 聲明并不包含在 ES6 中。官方的 Traceur 編譯器也不接受這種形式的代碼。
可以通過 let-er 工具將其轉換成合法的、可以工作的代碼。
let-er:一個構建時的代碼轉換器,但它 唯一 的作用就是找到 let 聲明并對其進行轉換。它不會處理包括 let 定義在內的任何其他代碼??梢园踩貙?let-er 應用在 ES6 代碼轉換的第一步,如果有必要,接下來也可以把代碼傳遞給 Traceur 等工具。
此外,let-er 還有一個設置項 – es6,開啟它(默認是關閉的)會改變生成代碼的種類。開啟這個設置項時 let-er 會生成完全標準的 ES6 代碼,而不會生成通過 try/catch 進行 hack 的 ES3 替代方案:
{ let a = 2;console.log( a ); } console.log( a ); // ReferenceError因此,在ES6 之前的所有環境中使用 let-er,開啟設置項就會生成標準的 ES6 代碼。
2. 附錄C this 詞法
ES6 中用箭頭函數的方式將this同詞法作用域聯系起來:
var foo = a => {console.log( a ); }; foo( 2 ); // 2普通函數容易丟失同this之間的綁定。解決該問題有好幾種辦法,但最常用
的是 var self = this;。
這里,self 是一個可以通過詞法作用域和閉包進行引用的標識符,它不關心this綁定的過程中發生了什么。
var obj = {count: 0,cool: function coolFn() {if (this.count < 1) {setTimeout( () => { // 箭頭函數是什么鬼東西?this.count++;console.log( "awesome?" );}, 100 );}} }; obj.cool(); // 很酷吧?箭頭函數 在涉及this綁定時的行為和 普通函數 的行為完全不一致,它放棄了所有普通this綁定的規則,取而代之的是用當前的詞法作用域覆蓋了this本來的值。
因此,上面這個箭頭函數只是“繼承”了cool() 函數的this綁定(調用它并不會出錯)。
箭頭函數缺點:它們是匿名而非具名的(具名函數比匿名函數更可取)。
因此,一個更合適的辦法是正確使用和包含this機制:
var obj = {count: 0,cool: function coolFn() {if (this.count < 1) {setTimeout( function timer(){this.count++; // this 是安全的// 因為bind(..)console.log( "more awesome" );}.bind( this ), 100 ); // look, bind()!}} }; obj.cool(); // 更酷了。無論你是喜歡箭頭函數中 this 詞法的新行為模式,還是喜歡更靠得住的 bind(),都需要注意箭頭函數不僅僅意味著可以少寫代碼。
二、this 和對象原型
this提供了一種優雅的方式來隱式“傳遞”一個對象引用,而顯式傳遞上下文對象會讓代碼變得越來越混亂。
1. 關于 this
1.1 關于的錯誤認識
太拘泥于“this”的字面意思就會產生一些誤解。主要存在兩種常見的對于this 的錯誤解釋。
-
誤解① - 指向自身
我們很容易把this理解成指向函數自身,這個推斷從英語的語法角度來說是說得通的。
那么為什么需要從函數內部引用函數自身呢?常見的原因是 遞歸(從函數內部調用這個函數)或者可以寫一個在第一次被調用后自己解除綁定的事件處理器。
下面是記錄函數foo被調用的次數的示例,可看到this并不像想像中的那樣指向函數本身:
function foo(num) {console.log( "foo: " + num );// 記錄foo 被調用的次數this.count++; } foo.count = 0; // 向對象foo添加屬性count,并賦值0 var i; for (i=0; i<10; i++) {if (i > 5) {foo( i );} }// 打印 foo 被調用次數 console.log( foo.count ); // 0 -- WTF?console.log 語句產生了4 條輸出,證明foo(..)確實被調用了4 次,但是foo.count仍然是0。顯然從字面意思來理解this是錯誤的。
執行foo.count = 0時,向函數對象 foo 添加了一個屬性 count,創建的是一個全局變量count。函數內部代碼this.count中的this并不是指向那個函數對象。
如果要從函數對象內部引用它自身,只使用 this 是不夠的。一般來說需要通過一個指向函數對象的詞法標識符(變量)來引用它。
function foo() {foo.count = 4; // foo 指向它自身 } setTimeout( function(){// 匿名(沒有名字的)函數無法指向自身 }, 10 );第一個函數被稱為具名函數,在它內部可以使用 foo 來引用自身。第二個傳入setTimeout(..) 的 回調函數 沒有名稱標識符(匿名函數),因此無法從函數內部引用自身。
因此,對于本例,另一種解決方法是使用 foo 標識符替代 this 來引用函數對象:
function foo(num) {console.log( "foo: " + num );// 記錄foo 被調用的次數foo.count++; } foo.count=0 var i; for (i=0; i<10; i++) {if (i > 5) {foo( i );} }然而,這種方法同樣回避了this的問題,并且完全 依賴于 變量 foo 的詞法作用域。
另一種方法是強制 this 指向 foo 函數對象:
function foo(num) {console.log( "foo: " + num );// 記錄foo 被調用的次數// 注意,在當前的調用方式下(參見下方代碼),this 確實指向foothis.count++; } foo.count = 0; var i; for (i=0; i<10; i++) {if (i > 5) {// 使用call(..) 可以確保 this 指向函數對象 foo 本身foo.call( foo, i );} }console.log( foo.count ); // 4
-
誤解② - this的作用域
第二種常見的誤解是,this 指向函數的作用域。這個問題有點復雜,因為在某種情況下它是正確的,但是在其他情況下它卻是錯誤的。
注意: this 在 任何情況下都不指向函數的詞法作用域 。
在 JavaScript 內部,作用域確實和對象類似,可見的標識符都是它的屬性。但是作用域“對象”無法通過 JavaScript 代碼訪問,它存在于JavaScript 引擎內部。
下面來看一個使用this來 隱式 引用函數的 詞法作用域 的示例:
function foo() {var a = 2;this.bar(); } function bar() {console.log( this.a ); }foo(); // ReferenceError: a is not defined這段代碼出自一個公共社區中互助論壇的精華代碼。這段代碼非常完美(同時也令人傷感)地展示了this 多么容易誤導人。
這里,它試圖跨界,但沒有成功!
- 首先,這段代碼試圖通過 this.bar() 來引用 bar() 函數。這是絕對不可能成功的。調用bar()最自然的方法是省略前面的this,直接使用詞法引用標識符。
- 此外,編寫這段代碼的開發者還試圖使用this 聯通foo() 和bar() 的詞法作用域,從而讓bar()可以訪問foo()作用域里的變量a。這是不可能實現的,你不能使用this來引用一個詞法作用域內部的東西。
因此,當你想要把this和詞法作用域的查找混合使用時,一定要提醒自己,這是無法實現的。
1.2 this 是什么
this是在運行時進行綁定的,并不是在編寫時綁定,它的上下文取決于函數調用時的各種條件。this的綁定和函數聲明的位置沒有任何關系,只取決于函數的調用方式 。
當一個函數被調用時,會創建一個活動記錄(有時候也稱為執行上下文)。這個記錄會包含函數 在哪里被調用(調用棧)、函數的調用方法、傳入的參數等信息。this就是記錄的其中一個屬性,會在函數執行的過程中用到。
【小結】
2. this 的全面解析
通過前面的學習,我們明白了每個函數的this是在調用時被綁定的,完全取決于函數的調用位置(也就是函數的調用方法)。
2.1 調用位置
理解調用位置:就是函數在代碼中被調用的位置(而不是聲明的位置)。
-
2.1.1 分析調用棧
只有仔細分析調用位置才能回答 “ 這個this到底引用的是什么?”
尋找調用位置,最重要的是要分析調用棧。來看看到底什么是調用棧和調用位置:
function baz() {// 當前調用棧是:baz// 因此,當前調用位置是全局作用域console.log( "baz" );bar(); // <-- bar 的調用位置 } function bar() {// 當前調用棧是baz -> bar// 因此,當前調用位置在baz 中console.log( "bar" );foo(); // <-- foo 的調用位置 } function foo() {// 當前調用棧是baz -> bar -> foo// 因此,當前調用位置在bar 中console.log( "foo" ); } baz(); // <-- baz 的調用位置要能分析出真正的調用位置的,因為它決定了this的綁定。
可以把調用棧想象成一個函數調用鏈,就像我們在前面代碼段的注釋中所寫的一樣。但是這種方法非常麻煩并且容易出錯。另一個查看調用棧的方法是使用瀏覽器的調試工具。絕大多數現代桌面瀏覽器都內置了開發者工具,其中包含JavaScript 調試器。就本例來說,你可以在工具中給foo() 函數的第一行代碼設置一個斷點,或者直接在第一行代碼之前插入一條debugger;語句。運行代碼時,調試器會在那個位置暫停,同時會展示當前位置的函數調用列表,這就是你的調用棧。因此,如果你想要分析this 的綁定,使用開發者工具得到調用棧,然后找到棧中第二個元素,這就是真正的調用位置。
-
2.1.2 綁定規則
先調用位置,再判斷 this 的綁定對象。而判斷需要應用下面四條規則中的一條:
- 默認綁定
- 隱式綁定
- 顯式綁定
- new綁定
1)默認綁定
獨立函數調用(無法應用其他規則時的默認規則,最常用)。對于直接使用不帶任何修飾的函數引用進行調用的,只能使用 默認綁定,無法應用其他規則。
如果使用嚴格模式(strict mode),全局對象將無法使用默認綁定,因此this會綁定到 undefined。
function foo() {"use strict";console.log( this.a ); } var a = 2; foo(); // TypeError: this is undefined這里,雖然this的綁定規則完全取決于調用位置,但是只有foo()運行在非strict mode下時,默認綁定才能綁定到全局對象;嚴格模式下與foo()調用位置無關 。
2)隱式綁定
隱式綁定 時,必須在一個對象內部包含一個指向函數的屬性,并通過這個屬性間接引用函數,從而把this間接(隱式)綁定到該對象上。
參數傳遞 就是一種隱式賦值,因此傳入函數的也會被隱式賦值;
當函數引用有上下文對象時, 隱式綁定 規則會把函數調用中的this綁定到這個上下文對象。
function foo() {console.log( this.a ); } var obj2 = {a: 42,foo: foo }; var obj1 = {a: 2,obj2: obj2 }; obj1.obj2.foo(); // 42對象屬性引用鏈中,只有最頂層或者最后一層會影響調用位置。
隱式丟失: 有個最常見的this綁定問題,就是被隱式綁定的函數會丟失綁定對象,也就是說它會應用默認綁定,從而把this綁定到全局對象或者undefined上(取決于是否是 嚴格模式)
function foo() {console.log( this.a ); } var obj = {a: 2,foo: foo }; var bar = obj.foo; // 函數別名! var a = "oops, global"; // a 是全局對象的屬性 bar(); // "oops, global"這里,bar是obj.foo的一個引用,但是它引用的是foo函數本身,此時的bar()是一個不帶任何修飾的函數調用,因此應用了 默認綁定 。
3)顯示綁定
如果不想在對象內部包含函數引用,而想 在某個對象上 強制調用函數,可以使用函數的call(..)和 apply(..)方法。
call(..)和apply(..)的工作原理:
它們的第1個參數是一個對象,這個對象會綁定到this,在調用函數時指定這個this。
像這種直接指定this綁定對象的綁定,就稱之為 顯式綁定 。
如下所示:
function foo() {console.log( this.a ); } var obj = {a:2 }; foo.call( obj ); // 21、從this綁定的角度來說,call(…) 和 apply(…) 是一樣的,它們的區別體現在其他的參數上。
-
① 硬綁定
顯式綁定仍然無法解決丟失綁定問題;但是,硬綁定(顯示綁定的一個變種)可以做到。
【工作原理】
Function.prototype.bind(..)會創建一個新的包裝函數,該函數會忽略它當前的this綁定(無論綁定的對象是什么),并把我們提供的對象綁定到this上。
如下所示:
function foo() {console.log( this.a ); } var obj = {a:2 }; var bar = function() {foo.call( obj ); }; bar(); // 2 setTimeout( bar, 100 ); // 2 // 硬綁定的bar 不可能再修改它的this bar.call( window ); // 2這里創建了函數bar(),并在它的內部手動調用了foo.call(obj),因此強制把foo的this綁定到了obj。無論之后如何調用函數bar,它總會手動在obj上調用foo。這種顯式的強制綁定,即稱之為 硬綁定 。
【硬綁定應用場景】
創建一個 包裹函數,傳入所有的參數并返回接收到的所有值:
function foo(something) {console.log( this.a, something );return this.a + something; } var obj = {a:2 }; var bar = function() {return foo.apply( obj, arguments ); }; var b = bar( 3 ); // 2 3 console.log( b ); // 5
由于硬綁定是一種非常常用的模式,所以在ES5中提供了內置的方法Function.prototype.bind,其用法如下:
function foo(something) {console.log( this.a, something );return this.a + something; } var obj = {a:2 }; var bar = foo.bind( obj ); var b = bar( 3 ); // 2 3 console.log( b ); // 5bind(..)的功能之一就是可以把除了第1個參數(第1個參數用于綁定this)之外的其他參數都傳給下層的函數(這種技術稱為“部分應用”,是“柯里化”的一種)。
bind(..)會返回一個硬編碼的新函數,它會把參數設置為this的上下文并調用原始函數。
-
② API 調用的上下文
第三方庫的許多函數,以及JavaScript 語言和宿主環境中許多新的內置函數,都提供了一個 可選的 參數,通常被稱為 “上下文 ”(context)。
作用: 和bind(..) 一樣,—— 確保回調函數使用指定的this。
示例如下:
function foo(el) {console.log( el, this.id ); } var obj = {id: "awesome" }; // 調用foo(..) 時把this 綁定到obj [1, 2, 3].forEach( foo, obj ); // 1 awesome 2 awesome 3 awesome這些函數實際上就是通過call(..)或者apply(..)實現了 顯式綁定,這樣可少些一些代碼。
-
4)new 綁定
在JavaScript中,構造函數 只是一些使用new操作符時被調用的普通 函數。它們并不屬于某個類,也不會實例化一個類。它們甚至都不能說是一種特殊的函數類型(new 的機制 實際上 和面向類的語言完全不同)。
使用new 來調用函數,或者說發生構造函數調用時,會自動執行下面的操作:
-
創建(構造)一個全新的對象;
-
這個新對象會被執行[[ 原型]] 連接;
-
這個新對象會綁定到函數調用的this;
-
如果函數沒有返回其他對象,那么new表達式中的函數調用會自動返回該新對象。
示例如下:
function foo(a) {this.a = a; } var bar = new foo(2); console.log( bar.a ); // 2當使用new來調用foo(..)時,會構造出一個新對象并把它綁定到foo(..)調用中的this上。new是最后一種可以 影響 函數調用時this綁定行為的方法,稱之為 new 綁定。
-
2.1.3 優先級
理解了函數調用中this綁定的四條規則,需要做的就是找到函數的調用位置并判斷應當應用哪條規則。
當某個調用位置可應用多條規則時,必須給這些規則設定 優先級 。
- 顯式綁定:優先級 > 隱式綁定;
- new綁定:優先級 > 隱式綁定;
- 隱式綁定:優先級 < 顯式綁定;
- 默認綁定:優先級 最低 ;
-
判斷 this
可按照下面的順序來進行判斷,函數在某個調用位置應用的是哪條規則。
-
函數是否在new中調用(new 綁定)?如果是,this綁定的則是新創建的對象。
var bar = new foo() -
函數是否通過call、apply(顯式綁定)或硬綁定調用?如果是,this綁定的是指定的對象。
var bar = foo.call(obj2) -
函數是否在某個上下文對象中調用(隱式綁定)?如果是,this綁定的是那個上下文對象。
var bar = obj1.foo() -
如果都不是,使用默認綁定。如果在嚴格模式下,就綁定到undefined,否則綁定到全局對象。
var bar = foo() -
2.1.4 綁定例外
某些場景下this的綁定行為會出乎意料,你認為應當應用其他綁定規則時,實際上應用的可能是默認綁定規則。
1)被忽略的this
如果把null或者undefined作為this的綁定對象傳入call、apply 或者bind,這些值在調用時會被忽略,其實際應用的是 默認綁定 規則。
示例如下:
function foo() {console.log( this.a ); } var a = 2; foo.call( null ); // 2什么情況下會傳入null 呢?
常見的做法是使用apply(..)來“展開”一個數組,并當作參數傳入一個函數。類似地,bind(..)可以對參數進行 柯里化(預先設置一些參數),該方法有時非常有用。如下所示:
function foo(a,b) {console.log( "a:" + a + ", b:" + b ); } // 把數組“展開”成參數 foo.apply( null, [2, 3] ); // a:2, b:3 // 使用 bind(..) 進行柯里化 var bar = foo.bind( null, 2 ); bar( 3 ); // a:2, b:3這兩種方法都需要傳入1個參數當作this的綁定對象 。如果函數并不關心this,仍然需要傳入一個占位值,這時null可能是一個不錯的選擇。
在ES6 中,可以用...操作符代替apply(..)來 “展開” 數組,foo(...[1,2])和foo(1,2)是一樣的,這樣可以避免不必要的this綁定。但是,在ES6中沒有柯里化的相關語法,因此仍需使用bind(..)。
如果總是使用null來忽略this綁定可能產生一些副作用。如果某個函數確實使用了this(比如第三方庫中的一個函數),那默認綁定規則會把this綁定到全局對象(在瀏覽器中這個對象是window),將導致不可預計的后果(比如修改全局對象)。因此,這種方式可能會導致許多難以分析和追蹤的bug。
-
2.1.5 this 詞法
前面介紹的四條規則包含所有正常的函數,但是, 箭頭函數 不使用 this 的4種標準規則,而是根據外層(函數或者全局)作用域來決定this。
來看箭頭函數的詞法作用域:
function foo() {// 返回一個箭頭函數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()內部創建的箭頭函數會捕獲調用時foo()的this。由于foo()的this綁定到obj1,bar(引用箭頭函數)的this也會綁定到obj1,箭頭函數的綁定無法被修改。(new也不行!)
箭頭函數最常用于回調函數中,例如事件處理器或者定時器:
function foo() {setTimeout(() => {// 這里的this 在此法上繼承自foo()console.log( this.a );},100); } var obj = {a:2 }; foo.call( obj ); // 2箭頭函數可以像bind(..)一樣確保函數的this被綁定到指定對象,此外,其重要性還體現在它用更常見的詞法作用域取代了傳統的this機制。
在 ES6 之前也使用一種幾乎和箭頭函數完全一樣的模式。示例如下:
function foo() {var self = this; setTimeout( function(){console.log( self.a );}, 100 ); } var obj = {a: 2 }; foo.call( obj ); // 2self = this和箭頭函數看起來都可以取代bind(..),但從本質上講,它們替代的是this機制。
如果你經常編寫this風格的代碼,但是絕大部分時候都會使用self = this或箭頭函數來否定this機制,那你或許應當:
- 只使用詞法作用域并完全拋棄錯誤this風格的代碼;
- 完全采用this風格,在必要時使用bind(..),盡量避免使用self = this和箭頭函數。
-
2.1.6 小結
如果要判斷一個運行中函數的this綁定,就需要找到這個函數的直接調用位置。找到之后 就可以順序應用下面這四條規則來判斷this的綁定對象。
- 由new 調用?綁定到新創建的對象。
- 由call 或者apply(或者bind)調用?綁定到指定的對象。
- 由上下文對象調用?綁定到那個上下文對象。
- 默認:在嚴格模式下綁定到undefined,否則綁定到全局對象。
對于正常的函數調用來說,理解了這些就可以明白this的綁定原理了。
但是,凡事都有例外。
2)更安全的this
一種 “更安全” 的做法,是傳入一個空的非委托對象,把this綁定到這個對象。
這樣,任何對于this的使用都會被限制在這個空對象中,不會對全局對象產生任何影響。由于這個對象完全是一個空對象,可以用?來命名這個變量,它非常形象,比null的含義更清楚。
要在JavaScript中創建一個空對象,最簡單的方法是:
Object.create(null)Object.create(null)和{}很像, 但是并不會創建Object.prototype這個委托,所以它比{}“更空”:
function foo(a,b) {console.log( "a:" + a + ", b:" + b ); } // 我們的DMZ 空對象 var ? = Object.create( null ); // 把數組展開成參數 foo.apply( ?, [2, 3] ); // a:2, b:3 // 使用bind(..) 進行柯里化 var bar = foo.bind( ?, 2 ); bar( 3 ); // a:2, b:33)間接引用
如果有意或無意地創建了一個函數的 “間接引用” 時,調用這個函數則會應用 默認綁定 規則。
間接引用最容易在 賦值時 發生:
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默認綁定。賦值表達式p.foo = o.foo的返回值是目標函數的引用,因此調用位置是foo() 而不是p.foo()或者o.foo() 。
注意:對于 默認綁定,決定this綁定對象的,并不是 調用位置 是否處于嚴格模式,而是 函數體 是否處于嚴格模式。如果函數體處于嚴格模式,this會被綁定到undefined,否則this會被綁定到全局對象。
4)軟綁定
硬綁定可把this強制綁定到指定的對象(除了使用new時),防止函數調用應用默認綁定規則。但是,硬綁定會大大降低函數的靈活性,使用硬綁定之后無法使用隱式綁定或顯式綁定來修改this。
軟綁定方法:
即給默認綁定指定一個全局對象和undefined以外的值,在實現和硬綁定相同的效果時,保留了隱式綁定或顯式綁定修改this的能力。
if (!Function.prototype.softBind) {Function.prototype.softBind = function(obj) {var fn = this;// 捕獲所有 curried 參數var curried = [].slice.call( arguments, 1 );var bound = function() {return fn.apply((!this || this === (window || global)) ?obj : thiscurried.concat.apply( curried, arguments ));};bound.prototype = Object.create( fn.prototype );return bound;}; }softBind(..)的其他原理和ES5 內置的bind(..)類似?!?它會對指定的函數進行封裝,首先檢查調用時的this,如果this綁定到全局對象或者undefined,那就把指定的默認對象obj綁定到this,否則不會修改this。
5)softBind實現軟綁定
示例代碼:
function foo() {console.log("name: " + this.name); } var obj = { name: "obj" }, obj2 = { name: "obj2" }, obj3 = { name: "obj3" }; var fooOBJ = foo.softBind( obj ); fooOBJ(); // name: obj obj2.foo = foo.softBind(obj); obj2.foo(); // name: obj2 <---- 看!!! fooOBJ.call( obj3 ); // name: obj3 <---- 看! setTimeout( obj2.foo, 10 ); // name: obj <---- 應用了軟綁定foo()可以手動將this綁定到obj2或者obj3上,但如果應用默認綁定,則會將this綁定到obj。
包含這兩種代碼風格的程序可以正常運行,但是在同一個函數或者同一個程序中混合使用這兩種風格會使代碼更難維護,可能也會更難寫。
一定要注意,有些調用可能在無意中使用默認綁定規則。如果想“更安全”地忽略this綁定,你可以使用一個DMZ對象,比如? = Object.create(null),以保護全局對象。
ES6 中的箭頭函數并不會使用四條標準的綁定規則,而是根據當前的詞法作用域來決定this,具體來說,箭頭函數會繼承外層函數調用的this綁定(無論this 綁定到什么)。這其實和ES6之前代碼中的self = this機制一樣。
3. 對象
我們知道函數調用位置的不同會造成this綁定對象的不同。那,對象到底是什么,綁定它們的目的是什么?
3.1 語法
對象的定義形式有 2 種:
-
聲明形式:
var myObj = {key: value// ... } -
構造形式:
var myObj = new Object(); myObj.key = value;
這兩種形式生成的對象的 區別 是:
-
聲明形式中,可以添加多個 鍵 / 值 對;
-
構造形式中,必須逐個添加屬性。
一般來說,用“構造形式”創建對象使用較少,使用聲明(文字)語法較常使用,包括絕大多數內置對象。
3.2 類型
在 JavaScript 中有 6 種主要數據類型:
- string
- number
- boolean
- null
- undefined
- object
除object外,其余 5 種簡單類型本身并不是對象,但 null 有時會被當做一種對象類型。這其實只是語言本身的一個bug,即對null執行typeof null時,會返回字符串"object" 1。
函數 是對象的一個 子類型(從技術角度來說就是“可調用的對象”)。JavaScript 中的函數是“一等公民”,因為其本質上和普通的對象一樣(只是可以調用),所以可以像操作其他對象一樣操作函數(比如當作另一個函數的參數)。
數組 也是對象的一種類型,具備一些額外的行為。數組中內容的組織方式比一般的對象要稍微復雜。
對象子類型 - 內置對象
有些內置對象的名字看起來和簡單基礎類型一樣,如下所示:
? String
? Number
? Boolean
? Object
? Function
? Array
? Date
? RegExp
? Error
這些內置對象,實際上只是一些內置函數??梢员划斪鳂嬙旌瘮?#xff08;由new產生的函數調用)來使用,從而可以構造一個對應子類型的新對象。
示例如下:
var strPrimitive = "I am a string"; typeof strPrimitive; // "string" strPrimitive instanceof String; // false var strObject = new String( "I am a string" ); typeof strObject; // "object" strObject instanceof String; // true // 檢查sub-type 對象 Object.prototype.toString.call( strObject ); // [object String]這里可以簡單地認為子類型在內部借用了Object中的toString()方法。
strObject是由String構造函數創建的一個對象。
類似于:
var strPrimitive = "I am a string";~ 引擎會自動把字面量 "I am a string" ,轉換成String 對象,使得我們可以訪問其屬性和方法:
console.log( strPrimitive.length ); // 13 console.log( strPrimitive.charAt( 3 ) ); // "m"對于數值字面量,如果使用類似42.359.toFixed(2) 的方法(該方法可把 Number 四舍五入 為指定小數位數的數字),引擎會把42轉換成new Number(42)。對于布爾字面量 來說也是如此。
null和undefined沒有對應的構造形式,只有文字形式。相反,Date只有構造,沒有文字形式。
對于Object、Array、Function和RegExp(正則表達式)來說,無論使用文字形式 還是構造形式,它們都是對象,不是字面量。
Error 對象很少在代碼中顯式創建,一般是在拋出異常時被自動創建。也可以使用new Error(…) 這種構造形式來創建,不過一般來說用不著。
3.3 對象內容 - 屬性
-
屬性訪問 和 鍵訪問
var myObject = {a: 2 }; myObject.a; // 2 myObject["a"]; // 2.a語法稱為屬性訪問;["a"]語法叫做鍵訪問。
兩種語法的 主要區別:
點(.)操作符要求屬性名滿足標識符的命名規范,而[".."] 語法可以接受任意UTF-8/Unicode字符串作為屬性名。
【示例】:如果要引用名稱為 “Super-Fun!” 的屬性,那就必須使用["Super-Fun!"] 語法訪問,因為Super-Fun!并不是一個有效的標識符屬性名。
由于[".."]語法使用字符串來訪問屬性,所以可以在程序中構造這個字符串。
示例如下:
var myObject = {a:2 }; var idx; if (wantA) {idx = "a"; } // 之后 console.log( myObject[idx] ); // 2在對象中,屬性名永遠都是字符串。如果使用string(字面量)以外的其他值作為屬性名,那它首先會被轉換為一個字符串(即使是數字也不例外)。
-
1)可計算屬性名
如果需要通過表達式來計算屬性名,那么myObject[..]這種屬性訪問語法,就可以派上用場了,如myObject[prefix + name]。
ES6 增加了可計算屬性名,可在文字形式中使用[]包裹一個表達式來當作屬性名:
var prefix = "foo"; var myObject = {[prefix + "bar"]:"hello",[prefix + "baz"]: "world" }; myObject["foobar"]; // hello myObject["foobaz"]; // world這是一種新的基礎數據類型,包含一個不透明且無法預測的字符串值。
-
2)屬性與方法
有些函數具有this引用,有時候這些this確實會指向調用位置的對象引用。但是這種用法從本質上來說并沒有把一個函數變成一個“方法”;
由于this是在運行時根據調用位置動態綁定的,所以 函數 和 對象 的關系最多也只能說是 間接關系 。
示例:
function foo() {console.log( "foo" ); } var someFoo = foo; // 對foo 的變量引用 var myObject = {someFoo: foo }; foo; // function foo(){..} someFoo; // function foo(){..} myObject.someFoo; // function foo(){..}someFoo和myObject.someFoo只是對于同一個函數的不同引用,并不能說明這個函數是特別的或者“屬于”某個對象。嚴謹地說,“函數” 和 “方法” 在 JavaScript 中是可以互換的。
注: 即使在 對象 的文字形式中聲明一個函數表達式,這個函數也不會“屬于”該對象 —— 它們只是對于相同函數對象的多個引用,如下所示:
var myObject = {foo: function() {console.log( "foo" );} }; var someFoo = myObject.foo; someFoo; // function foo(){..} myObject.foo; // function foo(){..}
-
3)數組
數組也支持[]訪問形式,不過數組有一套更加結構化的值存儲機制。數組期望的是數值下標,也就是說值存儲的位置(通常被稱為 索引 )是整數,比如說0和42:
var myArray = [ "foo", 42, "bar" ]; myArray.length; // 3 myArray[0]; // "foo" myArray[2]; // "bar"
數組 也是 對象 ,雖然每個下標都是整數,但仍然可以 添加屬性。
示例:
var myArray = [ "foo", 42, "bar" ]; myArray.baz = "baz"; myArray.length; // 3 myArray.baz; // "baz"這里雖然添加了命名屬性(無論是通過. 語法還是[]語法),數組的length值并未發生變化。
-
4)復制對象
思考一個對象:
function anotherFunction() { /*..*/ }var anotherObject = {c: true };var anotherArray = []; var myObject = {a: 2,b: anotherObject, // 引用,不是復本!c: anotherArray, // 另一個引用!d: anotherFunction }; anotherArray.push( anotherObject, myObject );
如何準確地表示myObject 的復制?
首先,應該判斷是淺復制還是深復制。
-
對于 淺拷貝,復制出的新對象中a的值會復制舊對象中a的值(2),但是新對象中b、c、d 三個屬性其實只是三個引用,和舊對象中b、c、d 引用的對象是一樣的。
-
對于 深拷貝,除了復制myObject以外,還會復制anotherObject和anotherArray。問題就來了,anotherArray引用了anotherObject和myObject,所以又需要復制myObject,就會由于循環引用導致 死循環。
我們是應該檢測循環引用、并終止循環(不復制深層元素)?還是應當直接報錯或是選擇其他方法?
對于JSON 安全(也就是說可以被序列化為一個JSON 字符串并且可以根據這個字符串解析出一個結構和值完全一樣的對象)的對象來說,有一種巧妙的復制方法:
var newObj = JSON.parse( JSON.stringify( someObj ) );但是,這種方法需要保證對象是JSON 安全的,所以只適用于部分情況。
相比深拷貝,淺拷貝易懂、并且問題要少得多。所以 ES6 定義了Object.assign(..)方法來實現淺拷貝。
Object.assign(..)方法的第1個參數是目標對象,之后還可以跟1個或多個源對象。它會遍歷1個或多個源對象的所有可枚舉的自有鍵,并把它們復制(使用= 操作符賦值)到目標對象,最后返回目標對象,示例如下:
var newObj = Object.assign( {}, myObject ); newObj.a; // 2 newObj.b === anotherObject; // true newObj.c === anotherArray; // true newObj.d === anotherFunction; // true由于Object.assign(…) 就是使用= 操作符來賦值,所以源對象屬性的一些特性(比如writable)不會被復制到目標對象。
-
~ 未完待續~
腳注:
null執行typeof null時,會返回字符串"object"原理是:
因不同的對象在底層都表示為二進制,而在JavaScript中,如二進制前三位都為0,則會被判斷為object類型,null的二進制表示是全0,自然前三位也是0,所以執行typeof時會返回“object”。 ??
總結
以上是生活随笔為你收集整理的你所不知道的 JavaScript的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 封装详解。
- 下一篇: JavaScript从入门到放弃 - E