隨着你的使用模式愈來愈複雜,顯式傳遞上下文對象會讓代碼變得愈來愈混亂,使用this
則不會這樣。當咱們介紹對象和原型時,你就會明白函數能夠自動引用合適的上下文對象有多重要。javascript
首先須要消除一些關於this的錯誤認識。html
先來看個例子:java
function foo(num) { console.log("foo: " + num); // 記錄foo被調用的次數,這裏this指向window 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被調用了多少次? console.log(foo.count); // 0 -- WTF?
執行foo.count = 0
時,的確向函數對象foo添加了一個屬性count。可是函數內部代碼this.count
中的this並非指向那個函數對象(指向全局變量 window),因此雖然屬性名相同,根對象卻並不相同,困惑隨之產生。es6
若是要從函數對象內部引用它自身,那隻使用this是不夠的。通常來講你須要經過一個指向函數對象的詞法標識符(變量)來引用它。算法
解決方法一:
一種解決方法是用詞法做用域:編程
function foo(num) { console.log( "foo: " + num ); // 記錄foo被調用的次數 data.count++; } var data = { count: 0 };
解決方法二:
另外一種解決方法是使用foo標識符替代this來引用函數對象:設計模式
function foo(num) { console.log( "foo: " + num ); // 記錄foo被調用的次數 foo.count++; } foo.count=0;
然而,這種方法一樣迴避了this的問題,而且徹底依賴於變量foo的詞法做用域。api
(重點)解決方法三:
另外一種方法是強制this
指向foo
函數對象:數組
function foo(num) { console.log( "foo: " + num ); // 記錄foo被調用的次數 // 注意,在當前的調用方式下(參見下方代碼),this確實指向foo this.count++; } foo.count = 0; var i; for (i=0; i<10; i++) { if (i > 5) { // 使用call(..)能夠確保this指向函數對象foo自己 foo.call( foo, i ); } } // foo: 6 // foo: 7 // foo: 8 // foo: 9 // foo被調用了多少次? console.log( foo.count ); // 4
第二種常見的誤解是,this指向函數的做用域。這個問題有點複雜,由於在某種狀況下它是正確的,可是在其餘狀況下它倒是錯誤的。瀏覽器
this在任何狀況下都不指向函數的詞法做用域。在JavaScript內部,做用域確實和對象相似,可見的標識符都是它的屬性。可是做用域「對象」沒法經過JavaScript代碼訪問,它存在於JavaScript引擎內部。
function foo() { var a = 2; this.bar(); } function bar() { console.log(this.a); } foo(); // ReferenceError: a is not defined
編寫這段代碼的開發者還試圖使用this聯通foo()和bar()的詞法做用域,從而讓bar()能夠訪問foo()做用域裏的變量a。這是不可能實現的,你不能使用this來引用一個詞法做用域內部的東西。
每當你想要把this
和詞法做用域
的查找混合使用時,必定要提醒本身,這是沒法實現的。
以前咱們說過this是在運行時進行綁定的,並非在編寫時綁定,它的上下文取決於函數調用時的各類條件。this的綁定和函數聲明的位置沒有任何關係,只取決於函數的調用方式。
當一個函數被調用時,會建立一個活動記錄(有時候也稱爲執行上下文)。這個記錄會包含函數在哪裏被調用(調用棧)、函數的調用方法、傳入的參數等信息。this就是記錄的其中一個屬性,會在函數執行的過程當中用到。
this
其實是在函數被調用時發生的綁定,它指向什麼徹底取決於函數在哪裏被調用。
最重要的是要分析調用棧(就是爲了到達當前執行位置所調用的全部函數)。咱們關心的調用位置就在當前正在執行的函數的前一個調用中。
function baz() { // 當前調用棧是:baz // 所以,當前調用位置是全局做用域 console.log("baz"+this.text); bar(); // <-- bar的調用位置 } function bar() { // 當前調用棧是baz -> bar // 所以,當前調用位置在baz中 console.log("bar"+this.text); foo(); // <-- foo的調用位置 } function foo() { debugger; // 當前調用棧是baz -> bar -> foo // 所以,當前調用位置在bar中 console.log("foo"+this.text); } baz(); // <-- baz的調用位置
function foo() { console.log(this.a); } var a = 2; foo(); // 2
函數調用時應用了this的默認綁定,所以this指向全局對象。
那麼咱們怎麼知道這裏應用了默認綁定呢?能夠經過分析調用位置來看看foo()是如何調用的。在代碼中,foo()是直接使用不帶任何修飾的函數引用進行調用的,所以只能使用默認綁定,沒法應用其餘規則。
function foo() { console.log(this.a); } var obj = { a: 2, foo: foo }; obj.foo(); // 2
不管你如何稱呼這個模式,當foo()被調用時,它的落腳點確實指向obj對象。
當函數引用有上下文對象時,隱式綁定規則會把函數調用中的this
綁定到這個上下文對象。由於調用foo()時this被綁定到obj,所以this.a和obj.a是同樣的。
隱式丟失
function foo() { console.log(this.a); } var obj = { a: 2, foo: foo }; var bar = obj.foo; // 函數別名! bar這裏調用的是全局foo() var a = "oops, global"; // a是全局對象的屬性 bar(); // "oops, global"
雖然bar是obj.foo的一個引用,可是實際上,它引用的是foo函數自己(obj.foo只是引用,沒有執行調用),所以此時的bar()實際上是一個不帶任何修飾的函數調用,所以應用了默認綁定。
不想在對象內部包含函數引用,而想在某個對象上強制調用函數,該怎麼作呢?
具體點說,可使用函數的
call(..)
和apply(..)
方法。它們的第一個參數是一個對象,它們會把這個對象綁定到this,接着在調用函數時指定這個this。由於你能夠直接指定this的綁定對象,所以咱們稱之爲顯式綁定。
惋惜,顯式綁定仍然沒法解決咱們以前提出的丟失綁定問題。
可是顯式綁定的一個變種能夠解決這個問題。
硬綁定的典型應用場景就是建立一個包裹函數,傳入全部的參數並返回接收到的全部值:
function foo(something) { console.log( this.a, something ); return this.a + something; } // 簡單的輔助綁定函數 function bind(fn, obj) { return function() { return fn.apply( obj, arguments ); }; } var obj = { a:2 }; var bar = bind( foo, obj ); 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 ); //這裏內置的 Function.prototype.bind var b = bar( 3 ); // 2 3 console.log( b ); // 5
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(..)實現了顯式綁定,這樣你能夠少些一些代碼。
在JavaScript中,構造函數只是一些使用new操做符時被調用的函數。
實際上並不存在所謂的「構造函數」,只有對於函數的「構造調用」。
使用new來調用函數,或者說發生構造函數調用時,會自動執行下面的操做。
(1) 建立一個新對象;
(2) 將構造函數的做用域賦給新對象(所以 this 就指向了這個新對象);
(3) 執行構造函數中的代碼(爲這個新對象添加屬性);
(4) 返回新對象。
function foo(a) { this.a = a; } var bar = new foo(2); console.log( bar.a ); // 2
使用new來調用foo(..)時,咱們會構造一個新對象並把它綁定到foo(..)
調用中的this
上。new是最後一種能夠影響函數調用時this綁定行爲的方法,咱們稱之爲new綁定。
判斷this四條規則的優先級,默認綁定最低。
隱式綁定 VS 顯式綁定
function foo() { console.log( this.a ); } var obj1 = { a: 2, foo: foo }; var obj2 = { a: 3, foo: foo }; obj1.foo(); // 2 obj2.foo(); // 3 obj1.foo.call( obj2 ); // 3 obj2.foo.call( obj1 ); // 2
當咱們使用call(obj2)顯式綁定時,輸出的值爲obj2的值(a=3),因此顯式綁定的優先級更高。
new綁定 VS 隱式綁定
function foo(something) { this.a = something; } var obj1 = { foo: foo }; var obj2 = {}; obj1.foo( 2 ); console.log( obj1.a ); // 2 obj1.foo.call( obj2, 3 ); console.log( obj2.a ); // 3 var bar = new obj1.foo( 4 ); console.log( obj1.a ); // 2 console.log( bar.a ); // 4
bar.a //4
能夠看到new綁定比隱式綁定優先級高.
new綁定 VS 顯示綁定
new和call/apply沒法一塊兒使用,所以沒法經過new foo.call(obj1)來直接進行測試。可是咱們可使用硬綁定來測試它倆的優先級。
function foo(something) { this.a = something; } var obj1 = {}; var bar = foo.bind(obj1); bar(2); console.log(obj1.a); // 2 var baz = new bar(3); console.log(obj1.a); // 2 console.log(baz.a); // 3
出乎意料!bar被硬綁定到obj1上,可是new bar(3)並無像咱們預計的那樣把obj1.a 修改成3。相反,new修改了硬綁定(到obj1的)調用bar(..)中的this。由於使用了new綁定,咱們獲得了一個名字爲baz的新對象,而且baz.a的值是3。
總結
new 綁定 > 顯示綁定 > 隱式綁定 > 默認綁定
箭頭函數並非使用function關鍵字定義的,而是使用被稱爲「胖箭頭」的操做符=>定義的。箭頭函數不使用this的四種標準規則,而是根據外層(函數或者全局)做用域來決定this。
箭頭函數的綁定沒法被修改。(new也不行!)
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!
其重要性還體如今它用更常見的詞法做用域取代了傳統的this機制。實際上,在ES6以前咱們就已經
在使用一種幾乎和箭頭函數徹底同樣的模式。
雖然self = this和箭頭函數看起來均可以取代bind(..),可是從本質上來講,它們想替代的是this機制。
若是你常常編寫this風格的代碼,可是絕大部分時候都會使用self = this或者箭頭函數來否認this機制,那你或許應當:
固然,包含這兩種代碼風格的程序能夠正常運行,可是在同一個函數或者同一個程序中混合使用這兩種風格一般會使代碼更難維護,而且可能也會更難編寫。
var myObj = { key: value // ... };
var myObj = new Object(); myObj.key = value;
JavaScript中一共有六種主要類型
• string
• number
• boolean
• null
• undefined
• object
前面5個是基本類型。
typeof null
時會返回字符串"object"(這是語言bug)。null
自己是基本類型。
JavaScript中有許多特殊的對象子類型,咱們能夠稱之爲複雜基本類型。
函數就是對象的一個子類型(從技術角度來講就是「可調用的對象」)。JavaScript中的函數是「一等公民」,由於它們本質上和普通的對象同樣(只是能夠調用),因此能夠像操做其餘對象同樣操做函數(好比看成另外一個函數的參數)。
數組也是對象的一種類型,具有一些額外的行爲。數組中內容的組織方式比通常的對象要稍微複雜一些。
JavaScript中還有一些對象子類型,一般被稱爲內置對象。
• String
• Number
• Boolean
• Object
• Function
• Array
• Date
• RegExp
• Error
這些內置函數能夠看成構造函數(由new產生的函數調用——參見第2章)來使用,從而能夠構造一個對應子類型的新對象。舉例來講:
var strPrimitive = "I am a string"; typeof strPrimitive; // "string" strPrimitive instanceof String; // false ,沒有new var strObject = new String( "I am a string" ); typeof strObject; // "object" strObject instanceof String; // true // 檢查sub-type對象 Object.prototype.toString.call( strObject ); // [object String]
原始值"I am a string"並非一個對象,它只是一個字面量,在必要時語言會自動把字符串字面量轉換成一個String對象,也就是說你並不須要顯式建立一個對象。JavaScript社區中的大多數人都認爲能使用文字形式時就不要使用構造形式。
var strPrimitive = "I am a string"; console.log( strPrimitive.length ); // 13 console.log( strPrimitive.charAt( 3 ) ); // "m"
引擎自動把字面量轉換成String對象,因此能夠訪問屬性和方法。number
與boolean
一樣如此。
null
和undefined
沒有對應的構造形式,它們只有文字形式。相反,Date
只有構造,沒有文字形式。
「內容」:存儲在對象容器內部的是這些屬性的名稱,它們就像指針(從技術角度來講就是引用)同樣,指向這些值真正的存儲位置。
var myObject = { a: 2 }; myObject.a; // 2 myObject["a"]; // 2
若是要訪問myObject中a位置上的值,咱們須要使用.操做符或者[]操做符。.a語法一般被稱爲「屬性訪問」,["a"]語法一般被稱爲「鍵訪問」。實際上它們訪問的是同一個位置,而且會返回相同的值2,因此這兩個術語是能夠互換的。
在對象中,屬性名永遠都是字符串。若是你使用string(字面量)之外的其餘值做爲屬性名,那它首先會被轉換爲一個字符串。即便是數字也不例外,
能夠在文字形式中使用[]包裹一個表達式來看成屬性名:
var prefix = "foo"; var myObject = { [prefix + "bar"]:"hello", [prefix + "baz"]: "world" }; myObject["foobar"]; // hello myObject["foobaz"]; // world
有些函數具備this引用,有時候這些this確實會指向調用位置的對象引用。可是這種用法從本質上來講並無把一個函數變成一個「方法」,由於this是在運行時根據調用位置動態綁定的,因此函數和對象的關係最多也只能說是間接關係。
不管返回值是什麼類型,每次訪問對象的屬性就是屬性訪問。若是屬性訪問返回的是一個函數,那它也並非一個「方法」。屬性訪問返回的函數和其餘函數沒有任何區別(除了可能發生的隱式綁定this,就像咱們剛纔提到的)。
function foo() { console.log(this + "foo"); } var someFoo = foo; // 對foo的變量引用 var myObject = { someFoo: foo }; foo(); // function foo(){..} someFoo(); // function foo(){..} myObject.someFoo(); // function foo(){..} 這裏的this隱式綁定object //不管哪一種引用形式都不能稱之爲「方法」。
數組也支持[]訪問形式,數組指望的是數值下標,也就是說值存儲的位置(一般被稱爲索引)是整數。
數組也是對象,因此雖然每一個下標都是整數,你仍然能夠給數組添加屬性:
var myArray = [ "foo", 42, "bar" ]; myArray.length; // 3 myArray.baz = "baz"; myArray.caz = "caz"; myArray.length; // 3 myArray.baz; // "baz"
雖然添加了命名屬性,數組的length值並未發生變化。
因此最好只用對象來存儲鍵/值
對,只用數組來存儲數值下標/值
對。
注意:若是你試圖向數組添加一個屬性,可是屬性名「看起來」像一個數字,那它會變成一個數值下標(所以會修改數組的內容而不是添加一個屬性):
var myArray = [ "foo", 42, "bar" ]; myArray["3"] = "baz"; myArray.length; // 4 myArray[3]; // "baz"
function anotherFunction() { /*..*/ } var anotherObject = { c: true }; var anotherArray = []; var myObject = { a: 2, b: anotherObject, // 引用,不是複本! c: anotherArray, // 另外一個引用! d: anotherFunction }; anotherArray.push( anotherObject, myObject );
首先,咱們應該判斷它是淺複製仍是深複製。
淺拷貝:複製出的新對象中a的值會複製舊對象中a的值,也就是2,可是新對象中b、c、d三個屬性其實只是三個引用,它們和舊對象中b、c、d引用的對象是同樣的。
深拷貝:除了複製myObject之外還會複製anotherObject和anotherArray。anotherArray引用了anotherObject和myObject,因此又須要複製myObject,這樣就會因爲循環引用致使死循環。
疑問:
1.咱們是應該檢測循環引用並終止循環(不復制深層元素)?
2.仍是應當直接報錯或者是選擇其餘方法?
JSON安全:
對於JSON安全(也就是說能夠被序列化爲一個JSON字符串而且能夠根據這個字符串解析出一個結構和值徹底同樣的對象)的對象來講,有一種巧妙的複製方法:
var newObj = JSON.parse( JSON.stringify( someObj ) );
這種方法須要保證對象是JSON安全的,因此只適用於部分狀況。
assign:賦值
淺複製很是易懂而且問題要少得多,因此ES6定義了Object.assign(..)
方法來實現淺複製。
Object.assign(..)
方法的第一個參數是目標對象,以後還能夠跟一個或多個源對象。它會遍歷一個或多個源對象的全部可枚舉(enumerable,參見下面的代碼)的自有鍵(owned key,很快會介紹)並把它們複製(使用=操做符賦值)到目標對象,最後返回目標對象,
var newObj = Object.assign( {}, myObject ); newObj.a; // 2 newObj.b === anotherObject; // true newObj.c === anotherArray; // true newObj.d === anotherFunction; // true
在ES5以前,JavaScript語言自己並無提供能夠直接檢測屬性特性的方法,好比判斷屬性是不是隻讀。
Descriptor:描述符號
var myObject = { a:2 }; Object.getOwnPropertyDescriptor( myObject, "a" ); // { // value: 2, // writable: true, // enumerable: true, // configurable: true // }
它還包含另外三個特性:writable(可寫)、enumerable(可枚舉)和configurable(可配置)。
咱們使用defineProperty(..)
給myObject添加了一個普通的屬性並顯式指定了一些特性。然而,通常來講你不會使用這種方式,除非你想修改屬性描述符
var myObject = {}; Object.defineProperty( myObject, "a", { value: 2, writable: true, configurable: true, enumerable: true } ); myObject.a; // 2
很重要的一點是,全部的方法建立的都是淺不變形,也就是說,它們只會影響目標對象和它的直接屬性。若是目標對象引用了其餘對象(數組、對象、函數,等),其餘對象的內容不受影響,仍然是可變的:
myImmutableObject.foo; // [1,2,3] myImmutableObject.foo.push( 4 ); myImmutableObject.foo; // [1,2,3,4]
假設代碼中的myImmutableObject
已經被建立並且是不可變的,可是爲了保護它的內容myImmutableObject.foo,你還須要使用下面的方法讓foo也不可變。
結合writable:false
和configurable:false
就能夠建立一個真正的常量屬性(不可修改、重定義或者刪除):
var myObject = {}; Object.defineProperty( myObject, "FAVORITE_NUMBER", { value: 42, writable: false, configurable: false } );
Object.preventExtensions() 方法讓一個對象變的不可擴展,也就是永遠不能再添加新的屬性。
若是你想禁止一個對象添加新屬性而且保留已有屬性,可使用Object.preventExtensions(..)
:
var myObject = { a:2 }; Object.preventExtensions( myObject ); myObject.b = 3; myObject.b; // undefined
Object.seal(..)
會建立一個「密封」的對象,這個方法實際上會在一個現有對象上調用Object.preventExtensions(..)
並把全部現有屬性標記爲configurable:false
。因此,密封以後不只不能添加新屬性,也不能從新配置或者刪除任何現有屬性(雖然能夠修改屬性的值)。
Object.freeze(..)
會建立一個凍結對象,這個方法實際上會在一個現有對象上調用Object.seal(..)
並把全部「數據訪問」屬性標記爲writable:false
,這樣就沒法修改它們的值。
(不過就像咱們以前說過的,這個對象引用的其餘對象是不受影響的,可使用「深度凍結」)。
對象常量:能夠再添加新的屬性
禁止擴展:不能再添加新的屬性
密封:不能再添加新的屬性,不能從新配置或者刪除任何現有屬性
凍結:不能再添加新的屬性,不能從新配置或者刪除任何現有屬性,不能修改它們的值
var myObject = { a: 2 }; myObject.a; // 2
myObject.a在myObject上其實是實現了[[Get]]操做。然而,若是沒有找到名稱相同的屬性,按照[[Get]]算法的定義會執行另一種很是重要的行爲。(其實就是遍歷可能存在的[[Prototype]]鏈,也就是原型鏈)
若是不管如何都沒有找到名稱相同的屬性,那[[Get]]操做會返回值undefined:
var myObject = { a:2 }; myObject.b; // undefined
注意,這種方法和訪問變量時是不同的。
若是你引用了一個當前詞法做用域中不存在的變量,並不會像對象屬性同樣返回undefined
,而是會拋出一個ReferenceError異常:
你可能會認爲給對象的屬性賦值會觸發[[Put]]來設置或者建立這個屬性。可是實際狀況並不徹底是這樣。
[[Put]]被觸發時,實際的行爲取決於許多因素,包括對象中是否已經存在這個屬性(這是最重要的因素)。
對象存在
對象不存在
第5章討論[[Prototype]]時詳細進行介紹。
當你給一個屬性定義getter、setter或者二者都有時,這個屬性會被定義爲「訪問描述符」(和「數據描述符」相對)。對於訪問描述符來講,JavaScript會忽略它們的value和writable特性,取而代之的是關心set和get(還有configurable和enumerable)特性。
var myObject = { // 給a定義一個getter get a() { return 2; } }; Object.defineProperty( myObject, // 目標對象 "b", // 屬性名 { // 描述符 // 給b設置一個getter get: function(){ return this.a * 2 }, // 確保b會出如今對象的屬性列表中 enumerable: true } ); myObject.a; // 2 myObject.b; // 4
前面咱們介紹過,如myObject.a的屬性訪問返回值多是undefined,可是這個值有多是屬性中存儲的undefined,也多是由於屬性不存在因此返回undefined。那麼如何區分這兩種狀況呢?
for..in循環能夠用來遍歷對象的可枚舉屬性列表(包括[[Prototype]]鏈)。可是如何遍歷屬性的值呢?
for循環
var myArray = [1, 2, 3]; for (var i = 0; i < myArray.length; i++) { console.log( myArray[i] ); } // 1 2 3
實際上並非在遍歷值,而是遍歷下標來指向值,如myArray[i]。
遍歷數組下標時採用的是數字順序(for循環或者其餘迭代器),可是遍歷對象屬性時的順序是不肯定的.必定不要相信任何觀察到的順序,它們是不可靠的。
ES5中增長了一些數組的輔助迭代器,包括forEach(..)
、every(..)
和some(..)
。
forEach(..)會遍歷數組中的全部值並忽略回調函數的返回值。
every(..)會一直運行直到回調函數返回false(或者「假」值)。
some(..)會一直運行直到回調函數返回true(或者「真」值)。
如何直接遍歷值而不是數組下標(或者對象屬性)呢?幸虧,ES6增長了一種用來遍歷數組的for..of循環語法:
var myArray = [ 1, 2, 3 ]; for (var v of myArray) { console.log( v ); } // 1 // 2 // 3
數組有內置的@@iterator
,所以for..of
能夠直接應用在數組上。
面向類的設計模式:實例化(instantiation)、繼承(inheritance)和(相對)多態(polymorphism)
若是你有函數式編程(好比Monad)的經驗就會知道類也是很是經常使用的一種設計模式。可是對於其餘人來講,這多是第一次知道類並非必須的編程基礎,而是一種可選的代碼抽象。
ES6中新增了一些元素,好比class關鍵字
這是否是意味着JavaScript中實際上有類呢?簡單來講:不是。
javascript中的類,是一種設計模式
在許多面向類的語言中,「標準庫」會提供Stack類,它是一種「棧」數據結構。可是在這些語言中,你實際上並非直接操做Stack,Stack類僅僅是一個抽象的表示,它描述了全部「棧」須要作的事,可是它自己並非一個「棧」你必須先實例化Stack類而後才能對它進行操做。
JavaScript中的對象有一個特殊的[[Prototype]]內置屬性,其實就是對於其餘對象的引用。幾乎全部的對象在建立時[[Prototype]]屬性都會被賦予一個非空的值。
var myObject = { a:2 }; myObject.a; // 2
在第3章中咱們說過,當你試圖引用對象的屬性時會觸發[[Get]]操做,好比myObject.a。對於默認的[[Get]]操做來講,第一步是檢查對象自己是否有這個屬性,若是有的話就使用它。
可是若是a不在myObject中,就須要使用對象的[[Prototype]]鏈了。
var anotherObject = { a:2 }; // 建立一個關聯到anotherObject的對象 var myObject = Object.create( anotherObject ); myObject.a; // 2
可是,若是anotherObject中也找不到a而且[[Prototype]]鏈不爲空的話,就會繼續查找下去。這個過程會持續到找到匹配的屬性名或者查找完整條[[Prototype]]鏈。若是是後者的話,[[Get]]操做的返回值是undefined。
使用for..in遍歷對象時原理和查找[[Prototype]]鏈相似,任何能夠經過原型鏈訪問到(而且是enumerable,參見第3章)的屬性都會被枚舉。使用in操做符來檢查屬性在對象中是否存在時,一樣會查找對象的整條原型鏈(不管屬性是否可枚舉):
var anotherObject = { a: 2 }; // 建立一個關聯到anotherObject的對象 var myObject = Object.create(anotherObject); // myObject.a; // 2 for (var k in myObject) { //這裏k,有"a" console.log("found: " + k); } // found: a ("a" in myObject); // true
全部普通的[[Prototype]]鏈最終都會指向內置的Object.prototype
。因此它包含JavaScript中許多通用的功能。好比說.toString()和.valueOf(),第3章還介紹過.hasOwnProperty(..)。稍後咱們還會介紹.isPrototypeOf(..),這個你可能不太熟悉。
給一個對象設置屬性並不只僅是添加一個新屬性或者修改已有的屬性值。
myObject.foo = "bar";
若是myObject對象中包含名爲foo的普通數據訪問屬性,這條賦值語句只會修改已有的屬性值。
若是foo不是直接存在於myObject中,[[Prototype]]鏈就會被遍歷,相似[[Get]]操做。
若是原型鏈上找不到foo,foo就會被直接添加到myObject上。
若是屬性名foo既出如今myObject中也出如今myObject的[[Prototype]]鏈上層,那麼就會發生屏蔽。myObject中包含的foo屬性會屏蔽原型鏈上層的全部foo屬性,由於myObject.foo老是會選擇原型鏈中最底層的foo屬性。
下面咱們分析一下若是foo不直接存在於myObject中而是存在於原型鏈上層時myObject.foo = "bar"會出現的三種狀況。
若是你但願在第二種和第三種狀況下也屏蔽foo,那就不能使用=操做符來賦值,而是使用Object.defineProperty(..)(參見第3章)來向myObject添加foo。
var anotherObject = { a: 2 }; var myObject = Object.create(anotherObject); anotherObject.a; // 2 myObject.a; // 2 anotherObject.hasOwnProperty("a"); // true myObject.hasOwnProperty("a"); // false myObject.a++; // 隱式屏蔽! anotherObject.a; // 2 myObject.a; // 3 myObject.hasOwnProperty("a"); // true
++操做至關於myObject.a = myObject.a + 1。
修改委託屬性時必定要當心。若是想讓anotherObject.a的值增長,惟一的辦法是 anotherObject.a++。
再說一遍,JavaScript中只有對象。
Foo.prototype
這個對象是在調用new Foo()
(參見第2章)時建立的,最後會被(有點武斷地)關聯到這個「Foo點prototype」對象上。
function Foo() { // ... } var a = new Foo(); Object.getPrototypeOf( a ) === Foo.prototype; // true
在面向類的語言中,實例化(或者繼承)一個類就意味着「把類的行爲複製到物理對象中」,對於每個新實例來講都會重複這個過程。
可是在JavaScript中,並無相似的複製機制。你不能建立一個類的多個實例,只能建立多個對象,它們[[Prototype]]關聯的是同一個對象。可是在默認狀況下並不會進行復制,所以這些對象之間並不會徹底失去聯繫,它們是互相關聯的。
實際上,絕大多數JavaScript開發者不知道的祕密是,new Foo()這個函數調用實際上並無直接建立關聯,這個關聯只是一個意外的反作用。new Foo()只是間接完成了咱們的目標:一個關聯到其餘對象的新對象。
那麼有沒有更直接的方法來作到這一點呢?固然!功臣就是Object.create(..)
。
關於名稱
在JavaScript中,咱們並不會將一個對象(「類」)複製到另外一個對象(「實例」),只是將它們關聯起來。從視覺角度來講,[[Prototype]]機制以下圖所示,箭頭從右到左,從下到上:
這個機制一般被稱爲原型繼承。
容易混淆的組合術語「原型繼承」(以及使用其餘面向類的術語好比「類」、「構造函數」、「實例」、「多態」,等等)嚴重影響了你們對於JavaScript機制真實原理的理解。
繼承意味着複製操做,JavaScript(默認)並不會複製對象屬性。相反,JavaScript會在兩
個對象之間建立一個關聯,這樣一個對象就能夠經過委託訪問另外一個對象的屬性和函數。委託(參見第6章)這個術語能夠更加準確地描述JavaScript中對象的關聯機制。
function Foo() { // ... } var a = new Foo();
究竟是什麼讓咱們認爲Foo是一個「類」呢?其中一個緣由是咱們看到了關鍵字new。看起來咱們執行了類的構造函數方法,Foo()的調用方式很像初始化類時類構造函數的調用方式。
Foo.prototype還有另外一個絕招。
function Foo() { // ... } Foo.prototype.constructor === Foo; // true var a = new Foo(); a.constructor === Foo; // true
Foo.prototype默認(在代碼中第一行聲明時!)有一個公有而且不可枚舉(參見第3章)的屬性.constructor
,這個屬性引用的是對象關聯的函數(本例中是Foo)。此外,咱們能夠看到經過「構造函數」調用new Foo()建立的對象a也有一個a.constructor
屬性,指向「建立這個對象的函數」。
當你在普通的函數調用前面加上new關鍵字以後,就會把這個函數調用變成一個「構造函數調用」。實際上,new會劫持全部普通函數並用構造對象的形式來調用它。
function NothingSpecial() { console.log( "Don't mind me!" ); } var a = new NothingSpecial(); // "Don't mind me!" a; // {}
NothingSpecial
只是一個普通的函數,可是使用new調用時,它就會構造一個對象並賦值給a,這看起來像是new的一個反作用(不管如何都會構造一個對象)。可是NothingSpecial自己並非一個構造函數。
在JavaScript中對於「構造函數」最準確的解釋是,全部帶new的函數調用。
函數不是構造函數,可是當且僅當使用new時,函數調用會變成「構造函數調用」。
回顧「構造函數」
以前討論.constructor屬性時咱們說過,看起來a.constructor === Foo爲真意味着a確實有一個指向Foo的.constructor屬性,可是事實不是這樣。
把.constructor屬性指向Foo看做是a對象由Foo「構造」很是容易理解,但這只不過是一種虛假的安全感。a.constructor只是經過默認的[[Prototype]]委託指向Foo,這和構造」毫無關係。相反,對於.constructor的錯誤理解很容易對你本身產生誤導。
舉例來講,Foo.prototype的.constructor屬性只是Foo函數在聲明時的默認屬性。若是你建立了一個新對象並替換了函數默認的.prototype對象引用,那麼新對象並不會自動得到.constructor屬性。
function Foo() { /* .. */ } Foo.prototype = { /* .. */ }; // 建立一個新原型對象 var a1 = new Foo(); //Object(..)並無「構造」a1 a1.constructor === Foo; // false! a1.constructor === Object; // true!
Object(..)並無「構造」a1,對吧?看起來應該是Foo()「構造」了它。若是你認爲「constructor」表示「由……構造」的話,a1.constructor應該是Foo,可是它並非Foo!
a1並無.constructor屬性,因此它會委託[[Prototype]]鏈上的Foo.prototype。可是這個對象也沒有.constructor屬性(不過默認的Foo.prototype對象有這個屬性!),因此它會繼續委託,此次會委託給委託鏈頂端的Object.prototype。這個對象有.constructor屬性,指向內置的Object(..)函數。
constructor並不表示被構造
實際上,對象的.constructor會默認指向一個函數,這個函數能夠經過對象的.prototype引用。「constructor」和「prototype」這兩個詞自己的含義可能適用也可能不適用。最好的辦法是記住這一點「constructor並不表示被構造」。
結論
a1.constructor是一個很是不可靠而且不安全的引用。一般來講要儘可能避免使用這些引用。
還記得這張圖嗎,它不只展現出對象(實例)a1到Foo.prototype的委託關係,還展現出Bar.prototype到Foo.prototype的委託關係,然後者和類繼承很類似,只有箭頭的方向不一樣。圖中由下到上的箭頭代表這是委託關聯,不是複製操做。
function Foo(name) { this.name = name; //調用位置:Foo.call(this, name); this綁定:2.call綁定,this爲 指定的對象,這裏的指定對象爲 Bar 對象 } Foo.prototype.myName = function () { return this.name; //調用位置:a.myName(); this綁定:3.上下文對象:a }; function Bar(name, label) { Foo.call(this, name); //調用位置:new Bar("a", "obj a"); this綁定:1.new綁定,this爲 Bar 對象 this.label = label; } // 咱們建立了一個新的Bar.prototype對象並關聯到Foo.prototype Bar.prototype = Object.create(Foo.prototype); // 注意!如今沒有Bar.prototype.constructor了 // 若是你須要這個屬性的話可能須要手動修復一下它 Bar.prototype.myLabel = function () { return this.label; //調用位置:a.myLabel(); this綁定:3.上下文對象:a }; var a = new Bar("a", "obj a"); a.myName(); // "a" a.myLabel(); // "obj a"
這段代碼的核心部分就是語句Bar.prototype = Object.create( Foo.prototype )。調用Object.create(..)會憑空建立一個「新」對象並把新對象內部的[[Prototype]]關聯到你指定的對象(本例中是Foo.prototype)。
換句話說,這條語句的意思是:「建立一個新的Bar.prototype對象並把它關聯到Foo.prototype」。
注意,下面這兩種方式是常見的錯誤作法,實際上它們都存在一些問題:
// 和你想要的機制不同! Bar.prototype = Foo.prototype; // 基本上知足你的需求,可是可能會產生一些反作用 :( 上面這裏會多個name:undefined Bar.prototype = new Foo();
ES6 的方法 Object.setPrototypeOf(..)
若是能有一個標準而且可靠的方法來修改對象的[[Prototype]]關聯就行了。在ES6以前,咱們只能經過設置.__proto__
屬性來實現,可是這個方法並非標準而且沒法兼容全部瀏覽器。ES6添加了輔助函數Object.setPrototypeOf(..)
,能夠用標準而且可靠的方法來修改關聯。
// ES6以前須要拋棄默認的Bar.prototype Bar.ptototype = Object.create( Foo.prototype ); // ES6開始能夠直接修改現有的Bar.prototype Object.setPrototypeOf( Bar.prototype, Foo.prototype );
假設有對象a,如何尋找對象a委託的對象(若是存在的話)呢?
檢查一個實例(JavaScript中的對象)的繼承祖先(JavaScript中的委託關聯)一般被稱爲內省(或者反射)。
function Foo() { // ... } Foo.prototype.blah = ...; var a = new Foo();
a instanceof Foo; // true
instanceof:在a的整條[[Prototype]]鏈中是否有指向Foo.prototype的對象?
這個方法只能處理對象(a)和函數(帶.prototype引用的Foo)之間的關係。只用instanceof沒法實現。
Foo.prototype.isPrototypeOf( a ); // true
在本例中,咱們實際上並不關心(甚至不須要)Foo,咱們只須要一個能夠用來判斷的對象(本例中是Foo.prototype)就行。
isPrototypeOf(..):在a的整條[[Prototype]]鏈中是否出現過Foo.prototype?
咱們只須要兩個對象就能夠判斷它們之間的關係。舉例來講:
// 很是簡單:b是否出如今c的[[Prototype]]鏈中? b.isPrototypeOf( c );
Object.getPrototypeOf(...)
與 __proto__
咱們也能夠直接獲取一個對象的[[Prototype]]鏈。在ES5中,標準的方法是:
Object.getPrototypeOf( a );
能夠驗證一下,這個對象引用是否和咱們想的同樣:
Object.getPrototypeOf( a ) === Foo.prototype; // true
絕大多數(不是全部!)瀏覽器也支持一種非標準的方法來訪問內部[[Prototype]]屬性:
a.__proto__ === Foo.prototype; // true
這個奇怪的.__proto__
(在ES6以前並非標準!)屬性「神奇地」引用了內部的[[Prototype]]對象,若是你想直接查找(甚至能夠經過.__proto__.__ptoto__
...來遍歷)原型鏈的話,這個方法很是有用。
和咱們以前說過的.constructor同樣,.__proto__
實際上並不存在於你正在使用的對象中(本例中是a)。實際上,它和其餘的經常使用函數(.toString()、.isPrototypeOf(..),等等)樣,存在於內置的Object.prototype中。(它們是不可枚舉的,參見第2章。)此外,.__proto__
看起來很像一個屬性,可是實際上它更像一個getter/setter(參見第3章)。
.__proto__
的實現大體上是這樣的(對象屬性的定義參見第3章):
Object.defineProperty( Object.prototype, "__proto__", { get: function() { return Object.getPrototypeOf( this ); }, set: function(o) { // ES6中的setPrototypeOf(..) Object.setPrototypeOf( this, o ); return o; } } );
所以,訪問(獲取值)a.__proto__
時,其實是調用了a.__proto__()
(調用getter函數)。雖然getter函數存在於Object.prototype對象中,可是它的this指向對象a(this的綁定規則參見第2章),因此和Object.getPrototypeOf( a )結果相同。
.__proto__
是可設置屬性,以前的代碼中使用ES6的Object.setPrototypeOf(..)進行設置。然而,一般來講你不須要修改已有對象的[[Prototype]]。
咱們只有在一些特殊狀況下(咱們前面討論過)須要設置函數默認.prototype對象的[[Prototype]],讓它引用其餘對象(除了Object.prototype)。這樣能夠避免使用全新的對象替換默認對象。此外,最好把[[Prototype]]對象關聯看做是隻讀特性,從而增長代碼的可讀性。
如今咱們知道了,[[Prototype]]機制就是存在於對象中的一個內部連接,它會引用其餘對象。
原型鏈:一般來講,這個連接的做用是:若是在對象上沒有找到須要的屬性或者方法引用,引擎就會繼續在[[Prototype]]關聯的對象上進行查找。同理,若是在後者中也沒有找到須要的引用就會繼續查找它的[[Prototype]],以此類推。這一系列對象的連接被稱爲「原型鏈」。
那[[Prototype]]機制的意義是什麼呢?
本章前面曾經說過Object.create(..)是一個大英雄,如今是時候來弄明白爲何了:
var foo = { something: function() { console.log( "Tell me something good..." ); } }; var bar = Object.create( foo ); bar.something(); // Tell me something good...
Object.create(..)會建立一個新對象(bar)並把它關聯到咱們指定的對象(foo),這樣咱們就能夠充分發揮[[Prototype]]機制的威力(委託)而且避免沒必要要的麻煩(好比使用new的構造函數調用會生成.prototype和.constructor引用)。
咱們並不須要類來建立兩個對象之間的關係,只須要經過委託來關聯對象就足夠了。而Object.create(..)不包含任何「類的詭計」,因此它能夠完美地建立咱們想要的關聯關係。
var anotherObject = { cool: function() { console.log( "cool!" ); } }; var myObject = Object.create( anotherObject ); myObject.cool(); // "cool!"
可是若是你這樣寫只是爲了讓myObject在沒法處理屬性或者方法時可使用備用的anotherObject,那麼你的軟件就會變得有點「神奇」,並且很難理解和維護。
當你給開發者設計軟件時,假設要調用myObject.cool(),若是myObject中不存在cool()時這條語句也能夠正常工做的話,那你的API設計就會變得很「神奇」,對於將來維護你軟件的開發者來講這可能不太好理解。
可是你可讓你的API設計不那麼「神奇」,同時仍然能發揮[[Prototype]]關聯的威力:
var anotherObject = { cool: function() { console.log( "cool!" ); } }; var myObject = Object.create( anotherObject ); myObject.doCool = function() { this.cool(); // 內部委託! 這裏this綁定myObject }; myObject.doCool(); // "cool!"
這裏咱們調用的myObject.doCool()是實際存在於myObject中的,這可讓咱們的API設計更加清晰(不那麼「神奇」)。從內部來講,咱們的實現遵循的是委託設計模式(參見第6章),經過[[Prototype]]委託到anotherObject.cool()。
內部委託比起直接委託可讓API接口設計更加清晰。
首先簡單回顧一下第5章的結論:[[Prototype]]機制就是指對象中的一個內部連接引用另外一個對象。
若是在第一個對象上沒有找到須要的屬性或者方法引用,引擎就會繼續在[[Prototype]]關聯的對象上進行查找。同理,若是在後者中也沒有找到須要的引用就會繼續查找它的[[Prototype]],以此類推。這一系列對象的連接被稱爲「原型鏈」
換句話說,JavaScript中這個機制的本質就是對象之間的關聯關係。
爲了更好地學習如何更直觀地使用[[Prototype]],咱們必須認識到它表明的是一種不一樣於類(參見第4章)的設計模式。
咱們須要試着把思路從類和繼承的設計模式轉換到委託行爲的設計模式。
Task = { setID: function (ID) { this.id = ID; }, outputID: function () { console.log(this.id); } }; // 讓XYZ委託Task XYZ = Object.create(Task); XYZ.prepareTask = function (ID, Label) { this.setID(ID); this.label = Label; }; XYZ.outputTaskDetails = function () { this.outputID(); console.log(this.label); }; XYZ.prepareTask(3, 100); XYZ.outputTaskDetails();
相比於面向類(或者說面向對象),我會把這種編碼風格稱爲「對象關聯」(OLOO,objects linked to other objects)。
如今你已經明白了「類」和「委託」這兩種設計模式的理論區別:
下面是典型的(「原型」)面向對象風格:
function Foo(who) { this.me = who; } Foo.prototype.identify = function() { return "I am " + this.me; }; function Bar(who) { Foo.call( this, who ); } Bar.prototype = Object.create( Foo.prototype ); Bar.prototype.speak = function() { alert( "Hello, " + this.identify() + "." ); }; var b1 = new Bar( "b1" ); var b2 = new Bar( "b2" ); b1.speak(); b2.speak();
下面咱們看看如何使用對象關聯風格來編寫功能徹底相同的代碼:
Foo = { init: function(who) { this.me = who; }, identify: function() { return "I am " + this.me; } }; Bar = Object.create( Foo ); Bar.speak = function() { alert( "Hello, " + this.identify() + "." ); }; var b1 = Object.create( Bar ); b1.init( "b1" ); var b2 = Object.create( Bar ); b2.init( "b2" ); b1.speak(); b2.speak();
下面咱們看看兩段代碼對應的思惟模型。
首先,類風格代碼的思惟模型強調實體以及實體間的關係:
下面咱們來看一張簡化版的圖,它更「清晰」一些——只展現了必要的對象和關係:
仍然很複雜,是吧?虛線表示的是Bar.prototype繼承Foo.prototype以後丟失的.constructor屬性引用(參見5.2.3節的「回顧‘構造函數’」部分),它們尚未被修復。即便移除這些虛線,這個思惟模型在你處理對象關聯時仍然很是複雜。
如今咱們看看對象關聯風格代碼的思惟模型:
經過比較能夠看出,對象關聯風格的代碼顯然更加簡潔,由於這種代碼只關注一件事:對象之間的關聯關係。
其餘的「類」技巧都是很是複雜而且使人困惑的。去掉它們以後,事情會變得簡單許多(同時保留全部功能)。
自省就是檢查實例的類型。
function Foo() { // ... } Foo.prototype.something = function(){ // ... } var a1 = new Foo(); // 以後 if (a1 instanceof Foo) { a1.something(); }
由於Foo.prototype(不是Foo!)在a1的[[Prototype]]鏈上(參見第5章),因此instanceof操做(會使人困惑地)告訴咱們a1是Foo「類」的一個實例。知道了這點後,咱們就能夠認爲a1有Foo「類」描述的功能。
從語法角度來講,instanceof彷佛是檢查a1和Foo的關係,可是實際上它想說的是a1和Foo.prototype(引用的對象)是互相關聯的。
var Foo = { /* .. */ }; var Bar = Object.create( Foo ); Bar... var b1 = Object.create( Bar );
使用對象關聯時,全部的對象都是經過[[Prototype]]委託互相關聯,下面是內省的方法,很是簡單:
// 讓Foo和Bar互相關聯 Foo.isPrototypeOf( Bar ); // true Object.getPrototypeOf( Bar ) === Foo; // true // 讓b1關聯到Foo和Bar Foo.isPrototypeOf( b1 ); // true Bar.isPrototypeOf( b1 ); // true Object.getPrototypeOf( b1 ) === Bar; // true
咱們沒有使用instanceof,由於它會產生一些和類有關的誤解。如今咱們想問的問題是「你是個人原型嗎?」咱們並不須要使用間接的形式,好比Foo.prototype或者繁瑣的Foo.prototype.isPrototypeOf(..)。
我以爲和以前的方法比起來,這種方法顯然更加簡潔而且清晰。再說一次,咱們認爲JavaScript中對象關聯比類風格的代碼更加簡潔(並且功能相同)。