this、對象原型

this和對象原型正則表達式

第一章 關於thischrome

1.1 爲何要用this設計模式

this 提供了一種更優雅的方式來隱式「傳遞」一個對象引用,所以能夠將 API 設計 得更加簡潔而且易於複用。顯式傳遞上下文對象會讓代碼變得愈來愈混亂,使用 this 則不會這樣。數組

1.2 誤解瀏覽器

1.2.1 指向自身安全

除了函數對象,還有更多更適合存儲狀態的地方。 函數表達式須要引用自身時(好比使用遞歸,或者定義本身的屬性和方法),應當要給函數表達式具名,而不是使用arguments.callee(已經被棄用和批判的用法)。app

function foo(num) {     
    console.log( "foo: " + num );    
    this.count++;  // 記錄 foo 被調用的次數
     // 記錄 foo 被調用的次數改成     
    // foo.count++;,能夠獲得正確答案4,但迴避了this問題
} 
foo.count = 0; 
var i; 
for (i=0; i<10; i++) {
    if (i > 5) {         
        foo( i );     
          // 使用 call(..) 能夠確保 this 指向函數對象 foo 自己
        // foo.call( 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 並非指向那個函數對象,因此雖然屬性名相同,根對象卻並不相 同,困惑隨之產生。

若是要從函數對象內部引用它自身,那隻使用 this 是不夠的。通常來講你須要經過一個指 向函數對象的詞法標識符(變量)來引用它。dom

1.2.2 它的做用域異步

this在任何狀況下都不指向函數的詞法做用域。ide

做用域「對象」沒法經過 JavaScript 代碼訪問,它存在於 JavaScript 引擎內部。

1.3 this究竟是什麼

this是在運行時綁定的,而不是在編寫時綁定,它的上下文取決於函數調用時的各類條件。this 的綁定和函數聲明的位置沒有任何關係,this只取決於函數的調用方式。

當一個函數被調用時,會建立一個活動記錄(有時候也稱爲執行上下文)。這個記錄會包 含函數在哪裏被調用(調用棧)、函數的調用方法、傳入的參數等信息。this 就是記錄的 其中一個屬性,會在函數執行的過程當中用到。

第二章 this全面解析

2.1 調用位置

調用位置是函數在代碼中被調用的位置,最重要的是要分析調用棧。 好比

function baz(){
    // 當前調用棧是baz,調用位置是全局做用域
    console.log(this); // window || global
    bar();
}

function bar(){
    // 當前調用棧是baz -> bar,調用位置在baz中
    // 能夠拿到baz詞法做用域的變量,可是和this無關
    console.log(this); // window || global
    foo();
}

function foo(){
    // 當前調用棧是baz -> bar -> foo,調用位置在bar中
    // 能夠拿到bar和baz詞法做用域的變量,可是和this無關
    console.log(this) // window || global
}
baz(); // baz的調用位置

2.2 綁定規則

2.2.1 默認綁定

this默認指向全局對象。 嚴格模式下,若是this找不到具體對象,就是undefined。 但若是函數聲明的區域不是嚴格模式的,即便在嚴格模式下調用,也不影響它將默認的this指向全局。

function foo() {     
    console.log( this.a ); 
} 
var a = 2; 
foo(); // 2
//當調用 foo() 時,this.a 被解析成了全局變量 a。函數調用時應用了 this 的默認綁定,所以 this 指向全局對象。
//。在代碼中,foo() 是直接使用不帶任何修飾的函數引用進行調用的,所以只能使用 默認綁定,沒法應用其餘規則。

//若是使用嚴格模式(strict mode),那麼全局對象將沒法使用默認綁定
//此 this 會綁定 到 undefined:
function foo() {      
    "use strict"; 
     console.log( this.a ); 
} 
 var a = 2; 
 foo(); // TypeError: this is undefined

2.2.2 隱式綁定

this隱式綁定的,是整個調用鏈上,處於方法上一層的對象。

function foo(){
    console.log(this.a)
}

var obj = {
    a: 2,
    foo: foo,
};

obj.foo(); //2
//其實等效於,JS的函數調用其實更像是一種語法糖
obj.foo.call(obj);
//當函數引 用有上下文對象時,隱式綁定規則會把函數調用中的 this 綁定到這個上下文對象。由於調 用 foo() 時 this 被綁定到 obj,所以 this.a 和 obj.a 是同樣的。

隱式丟失

須要特別注意的是,回調函數丟失this綁定是很是常見的

function foo() {     
    console.log( this.a );
} 
function doFoo(fn) {     
    // fn 其實引用的是 foo 
    fn(); // <-- 調用位置!
} 
var obj = {     
    a: 2,     
    foo: foo  
}; 
var a = "oops, global"; // a 是全局對象的屬性 
doFoo( obj.foo ); // "oops, global"
//參數傳遞其實就是一種隱式賦值,所以咱們傳入函數時也會被隱式賦值

由於調用它的操做一般不包含隱式綁定和顯式綁定。 這個時候能夠選擇:

  • ES6的lambda表達式
  • 從外部提供var _this = this變量來注入進去
  • bind顯式綁定

2.2.3 顯式綁定

call和apply的效果同樣,傳入參數的方式不同。 call從第二個參數開始,逐個傳入。 apply的第二個參數是一個Array,參數放在Array裏。 bind的參數形式相似於call,可是返回的是顯式綁定後的函數對象,須要後續去調用執行。 bind經常使用於柯里化一個函數對象。

硬綁定

建立了函數 bar(),並在它的內部手動調用 了 foo.call(obj),所以強制把 foo 的 this 綁定到了 obj。不管以後如何調用函數 bar,它 總會手動在 obj 上調用 foo。這種綁定是一種顯式的強制綁定,所以咱們稱之爲硬綁定。

【硬綁定的典型應用場景】就是建立一個包裹函數,傳入全部的參數並返回接收到的全部值;另外一種使用方法是建立一個 i 能夠重複使用的輔助函數

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



// ES6寫法
Function.prototype._bind = function(thisArg, ...args){
    return (...newArgs) => this.call(thisArg, ...args, ...newArgs);
}

// ES5寫法
Function.prototype._bind = function(thisArg){
    var args = [].slice.call(arguments, 1);
    var fn = this;
    return function(){
        var newArgs = [].slice.call(arguments, 0);
        return fn.apply(thisArg, args.concat(newArgs));
    };
}

//MDN提供的polyfill
if(!Function.prototype.bind) {
    Function.prototype.bind = function(oThis) {
        // 類型判斷
        if(typeof this !== 'function'){
            throw new TypeError('Function.prototype.bind - what is trying to be found is not callable');
        }
        
        var aArgs = Array.prototype.slice.call(arguments, 1),
            fToBind = this,
            fNOP = function(){},
            fBound = function(){
                return fToBind.apply((
                    this instanceof fNOP && oThis? this: oThis
                ),
                aArgs.concat(Array.prototype.slice.call(arguments))
                );
            };
        
        // 繼承原型鏈
        fNOP.prototype = this.prototype;
        fBound.prototype = new fNOP();

        return fBound;
    }
}

2.2.4 new綁定

實際上並不存在所謂的「構造函數」,只有對於函數的「構造調用」。

用new來調用函數,會自動執行下面的操做:

  1. 建立(或者說構造)一個全新的對象。
  2. 這個新對象會被執行[[Prototype]]連接。
  3. 這個新對象會綁定到函數調用的this。
  4. 若是函數沒有返回其餘對象,那麼new表達式中的函數調用會自動返回這個新對象。

    function foo(a) {
    this.a = a;
    }
    var bar = new foo(2);
    console.log( bar.a ); // 2
    //new 來調用 foo(..) 時,咱們會構造一個新對象並把它綁定到 foo(..) 調用中的 this 上

2.3 優先級

  • new在調用構造函數的時候作初始綁定。
  • 顯式綁定或硬綁定優先級最高。
  • 沒有顯式綁定,就使用隱式綁定。
  • 若是都沒有,就使用默認綁定(undefined,是否是全局看嚴格模式)。

2.4 綁定例外

2.4.1 被忽略的this (空對象)

顯式綁定中,若是對this不敏感,能夠傳入null,可是可能有反作用。 更安全的作法是,傳入一個空對象,即var empty = Object.create(null),它連指向Object.prototype的proto都沒有,比{}更空。

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:3
//使用變量名 ø 不只讓函數變得更加「安全」,並且能夠提升代碼的可讀性,由於 ø 表示 「我但願 this 是空」,這比 null 的含義更清楚。

2.4.2 間接引用

(p.foo = o.foo)()這裏返回的引用是foo,而不是p.foo,嚴格模式下,它的this指向會指向undefined。

2.4.3 軟綁定

硬綁定會大大下降函數的靈活性,使 用硬綁定以後就沒法使用隱式綁定或者顯式綁定來修改 this。

軟綁定,能夠實現和硬綁定相 同的效果,同時保留隱式綁定或者顯式綁定修改 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 : this    
                curried.concat.apply( curried, arguments ) 
            );       
        };       
        bound.prototype = Object.create( fn.prototype );  
        return bound;      
    }; 
}

2.5 this詞法

lambda表達式(箭頭函數)的this是根據外層做用域來決定。 lambda表達式經常使用於回調函數,好比eventListener和setTimeout操做中。

function foo() {
    // 返回一個箭頭函數
    return (a) => {
        // 這裏的this實際上是foo中的this
        console.log(this.a);
    };
}

var obj1 = {a:2},
    obj2 = {a:3};

// bar的this指向foo的this,foo的this此時顯式綁定了obj1
var bar = foo.call(obj1);
// 雖然顯式綁定了bar的this,可是這種方法對箭頭函數不起做用
// 它的this依然指向先前foo被顯式綁定的obj1
bar.call(obj2); //2

箭頭函數最經常使用於回調函數中,例如事件處理器或者定時器

ES6 中的箭頭函數並不會使用四條標準的綁定規則,而是根據當前的詞法做用域來決定 this,具體來講,箭頭函數會繼承外層函數調用的 this 綁定(不管 this 綁定到什麼)。這 其實和 ES6 以前代碼中的 self = this 機制同樣。

第三章 對象

3.1 語法

//文字語法(聲明形式)(建立簡單對象時推薦使用)
var myObj = {
    key: value,
};

//構造形式
var myObj = new Object();
myObj.key = value;

3.2 類型

JS一共有七種主要類型(語言類型):

  • string
  • number
  • boolean
  • undefined
  • null
  • object
  • symbol

注意,簡單基本類型(string、boolean、number、null 和 undefined)自己並非對象。 null 有時會被看成一種對象類型,可是這其實只是語言自己的一個 bug,即對 null 執行 typeof null 時會返回字符串 "object"。1 實際上,null 自己是基本類型。

❤ 注 1: 原理是這樣的,不一樣的對象在底層都表示爲二進制,在 JavaScript 中二進制前三位都爲 0 的話會被判 斷爲 object 類型, null 的二進制表示是全 0,天然前三位也是 0,因此執行 typeof 時會返回「object」。

console.log(typeof null); //object
console.log(typeof string);  //undefined
console.log(typeof number); //undefined
console.log(typeof String); //function
console.log(typeof Number);//function

內置對象(注意第一個字母大寫,這些都是構造函數):

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

在 JavaScript 中,它們實際上只是一些內置函數。這些內置函數能夠看成構造函數 (由 new 產生的函數調用——參見第 2 章)來使用,從而能夠構造一個對應子類型的新對 象。

補充:js中的基本類型和引用類型

var strPrimitive = "I am a string";  
typeof strPrimitive; // "string"  
strPrimitive instanceof String; // false 
//typeof:肯定變量是字符串、數值、布爾值仍是undefined的最佳工具。
//instanceof :判斷是不是某個對象類型。
var strObject = new String( "I am a string" );  
typeof strObject; // "object" 
strObject instanceof String; // true 

// 檢查 sub-type 對象 
Object.prototype.toString.call( strObject ); // [object String]
  • 在必要時語言會自動把字符串字面量轉換成一個 String 對象(如獲取長度、訪問某個字符等),也就是說你並不須要 顯式建立一個對象。JavaScript 社區中的大多數人都認爲能使用文字形式時就不要使用構 造形式。
  • 若是使用相似 42.359.toFixed(2) 的方法,引擎會把對象 42 轉換成 new Number(42)。對於布爾字面量來講也是如此。
  • null 和 undefined 沒有對應的構造形式,它們只有文字形式。相反,Date 只有構造,沒有 文字形式。
  • 對於 Object、Array、Function 和 RegExp(正則表達式)來講,不管使用文字形式仍是構 造形式,它們都是對象,不是字面量。
  • Error 對象不多在代碼中顯式建立,通常是在拋出異常時被自動建立。也可使用 new Error(..) 這種構造形式來建立,不過通常來講用不着。

3.3 內容

  • 對象的內容是由一些存儲在特定命名位置的值組成的,咱們稱之爲屬性。
  • 存儲在對象容器內部的是這些屬性的名稱,它們就像指針同樣,指向這些值真正的存儲位置。
  • .key方式稱爲屬性訪問,['key']方式稱爲鍵訪問。 屬性訪問更直接和直觀,可是其屬性名有必定的規範;鍵訪問容許傳入動態的字符串變量來訪問,只要是字符串便可,更靈活。
  • 在對象中,屬性名永遠都是字符串。若是你使用 string(字面量)之外的其餘值做爲屬性 名,那它首先會被轉換爲一個字符串。即便是數字也不例外

3.3.1 可計算屬性名

ES6支持在鍵訪問中傳入一個表達式來看成屬性名,好比myObj[prefix + 'foo']。在文字形式中使用 [] 包裹一個表達式來看成屬性名

var prefix = "foo"; 
var myObject = {    
    [prefix + "bar"]:"hello",   
}; 
myObject["foobar"]; // hello

3.3.2 屬性與方法

屬於對象的函數一般稱爲方法。

即便你在對象的文字形式中聲明一個函數表達式,這個函數也不會「屬於」這個對象—— 它們只是對於相同函數對象的多個引用

3.3.3 數組

數組經過數字下標[索引]訪問。 數組能夠添加命名屬性,可是不會改變其length值。

var myArray = [ "foo", 42, "bar" ];  
myArray.baz = "baz";  
myArray.length; // 3 
myArray.baz; // "baz"

注意:若是你試圖向數組添加一個屬性,可是屬性名「看起來」像一個數字,那它會變成 一個數值下標(所以會修改數組的內容而不是添加一個屬性):

var myArray = [ "foo", 42, "bar" ];  
myArray["3"] = "baz";  
myArray.length; // 4 
myArray[3]; // "baz"

3.3.4 複製對象

  • 對於JSON安全的對象來講,可使用
    var cloned = JSON.parse(JSON.stringify(target))的方式。
    固然,這種方法須要保證對象是 JSON 安全的,因此只適用於部分狀況。
  • ES6中可使用Object.assign(目標對象,源對象)方法(淺複製)
    因爲 Object.assign(..) 就是使用 = 操做符來賦值,所 以源對象屬性的一些特性(好比 writable)不會被複制到目標對象。

    const cloned = Object.assign(
    // 生成原型鏈
    Object.create(Object.getPrototypeOf(target)),
    target
    );

須要注意的是,PropertyDescriptor不會被按原樣複製,而是保持默認值。

3.3.5 屬性描述符

從 ES5 開始,全部的屬性都具有了屬性描述符。

var myObject = { a:2 }; 
Object.getOwnPropertyDescriptor( myObject, "a" );  
 { 
    value: 2, 
    writable: true, //writable(可寫)
    enumerable: true, //enumerable(可枚舉)
    configurable: true // configurable(可配置)
}
//也可使用 Object.defineProperty(..) 來添加一個新屬性或者修改一個已有屬性(若是它是 configurable)並對特性進行設置
var myObject = {}; 
 Object.defineProperty( myObject, "a", {     
     value: 2,     
     writable: true,     
     configurable: true,      
     enumerable: true
 } ); //只要屬性是可配置的,就可使用 defineProperty(..)

一個descriptor有6個可能的屬性,同時只能擁有其中4個(分爲基本類型和引用類型):

  • value,屬性值(只能用於基本類型)
  • writable,是否能夠寫入,false表示readonly,默認true(只能用於基本類型),若是在嚴格模式下,寫入操做會報錯TypeError
  • get,getter方法(只能用於引用類型)
  • set,setter方法(只能用於引用類型)
  • configurable,descriptor是否能夠修改(或者是刪除),默認true。把 configurable 修改爲 false 是單向操做,沒法撤銷!(一個小小的例外:即使屬性是 configurable:false, 咱們仍是能夠 把 writable 的狀態由 true 改成 false,可是沒法由 false 改成 true。)
  • enumerable,是否爲可枚舉屬性,關係到可否出現於for...of、for...in等操做中,默認true

3.3.6 不變性

  1. 結合writable: false和configurable: false就能夠建立一個真正的常量屬性。

    var myObject = {};

    Object.defineProperty( myObject, 'Favorite_Number', {
    value: 22,
    writable: false,
    configurable: false,
    });

  2. 禁止擴展,使用Object.preventExtensions( targetObject ),不能再添加新屬性。(在嚴格模式下,添加新屬性將會拋出 TypeError 錯誤。)
  3. 密封,使用Object.seal( targetObject ),只能修改值,不能添加/刪除或從新配置屬性。(實際上會在一個現有對象上調用 Object.preventExtensions(..) 並把全部現有屬性標記爲 configurable:false。)
  4. 凍結,使用Object.freeze( targetObject ),在seal基礎上在將屬性改成writable: false,只讀。(「深度凍結」一個對象,具體方法爲,首先在這個對象上調用 Object.freeze(..), 而後遍歷它引用的全部對象並在這些對象上調用 Object.freeze(..)。)

3.3.7 [[Get]]

  • 當給對象進行一次屬性訪問時(如:myObject.a ),實際上是使用了[[Get]]操做(有點像函數調 用:[Get])。
  • 對象默認的Get操做首先在對象中查找是否有名稱相同的屬性,若是找到就返回;
  • 沒有找到,就會去原型鏈上尋找;
  • 若是不管如何都沒有找到名稱相同的屬性,那 [[Get]] 操做會返回值 undefined

3.3.8 [[Put]]

給對象的屬性賦值會觸發 [[Put]] 來設置或者建立這個屬性(實際狀況複雜)。

若是屬性已經存在,Put會檢查:

  1. 屬性是不是訪問描述符,若是是,而且有setter,就調用setter。
  2. 屬性的數據描述符中writable是否爲false?若是爲false,非嚴格模式下靜默失敗,嚴格模式拋出錯誤。
  3. 若是都不是,將該值設置爲屬性的值。
  4. 若是對象中不存在這個屬性,[[Put]] 操做會更加複雜。

3.3.9 Getter和Setter

在 ES5 中可使用 getter 和 setter 部分改寫默認操做,可是隻能應用在單個屬性上,沒法 應用在整個對象上。

getter 是一個隱藏函數,會在獲取屬性值時調用。setter 也是一個隱藏 函數,會在設置屬性值時調用。

當一個屬性定義 getter、setter 或者二者都有時,這個屬性會被定義爲「訪問描述 符」(和「數據描述符」相對)。對於訪問描述符來講,JavaScript 會忽略它們的 value 和 writable 特性,取而代之的是關心 set 和 get(還有 configurable 和 enumerable)特性。

定義方式

// 直接式
var myObject = {
    // 給a定義一個getter,此時再去訪問a,只會得到2
    get a(){
        return 2;
    },
    // 不是單純的setter,而會取兩倍的值
    set a(val){
        this._a_ = val * 2;
    }
    //即使有合法的 setter,因爲咱們自定義的 getter 只會返回 2,因此 set 操做是 沒有意義的。
}


// descriptor式
Object.defineProperty(myObject, 'b', {
    get: function(){
        return this._b_;
        //名稱 _b_ 只是一種慣例,沒有任何特殊的行爲——和其餘普通屬性 同樣。
    },
    set: function(val){
        this._b_ = val*2;
    },
    enumerable: true,//確保b會出如今對象的屬性列表中
});
myObject.b = 2
console.log(myObject.b)//4

3.3.10 存在性(如何區分undefined)

  • 如何區分 myObject.a 的屬性訪問返回值: undefined
    1. 是屬性中存儲的 undefined
    2. 是由於屬性不存在

    ("a" in myObject); // true
    ("b" in myObject); // false

    myObject.hasOwnProperty( "a" ); // true
    myObject.hasOwnProperty( "b" ); // false

  • in操做符會檢查屬性是否在對象及其[[Prototype]]原型鏈中。須要注意的是,in只檢查鍵是否存在,好比在Array中,並不會檢查Array的內容,而是檢查下標。
  • hasOwnProperty只會檢查屬性是否在對象中,不會檢查原型鏈。

3.3.11 可枚舉性

可枚舉,至關於能夠出如今對象屬性的遍歷中。 propertyIsEnumerable會檢查給定的屬性名是否直接存在於對象中(而不是原型鏈中),而且知足enumerable: true。

var myObject = { };  

Object.defineProperty(     
    myObject,     "a",    
    // 讓 a 像普通屬性同樣能夠枚舉     
    { enumerable: true, value: 2 }
); 

Object.defineProperty( 
    myObject,     "b",    
    // 讓 b 不可枚舉     
    { enumerable: false, value: 3 } ); 

myObject.b; // 3 
("b" in myObject); // true  
myObject.hasOwnProperty( "b" ); // true 
//in 和 hasOwnProperty(..) 的區別在因而否查找 [[Prototype]] 鏈
 
for (var k in myObject) {     
    console.log( k, myObject[k] ); 
} // "a" 2
//myObject.b 確實存在而且有訪問值,可是卻不會出如今 for..in 循環中

myObject.propertyIsEnumerable( "a" ); // true 
myObject.propertyIsEnumerable( "b" ); // false  

Object.keys( myObject ); // ["a"] 
//Object.keys(..) 會返回一個數組,包含全部可枚舉屬性
Object.getOwnPropertyNames( myObject ); // ["a", "b"]
//Object.getOwnPropertyNames(..) 會返回一個數組,包含全部屬性,不管它們是否可枚舉。

3.4 遍歷

  • for...in:遍歷全部可遍歷的屬性名(包括 [[Prototype]] 鏈)(不是值)。
  • for...of:遍歷全部可遍歷屬性的值。(ES6新增),數組有內置的 @@iterator,所以 for..of 能夠直接應用在數組上(見下例)。
  • foreach,every,some,map,filter,reduce等委託的原型方法則是將遍歷改成鏈式調用。
    1. forEach(..) 會遍歷數組中的全部值並忽略回調函數的返回值。
    2. every(..) 會一直運行直到回調函數返回 false(或者「假」值)
    3. some(..) 會一直運行直到回調函數返回 true(或者 「真」值)。
    4. every(..) 和 some(..) 中特殊的返回值和普通 for 循環中的 break 語句相似,它們會提早 終止遍歷

使用Symbol.iterator對象,能夠用來定義對象的@@iterator內部屬性:

var myArray = [ 1, 2, 3 ]; 
var it = myArray[Symbol.iterator](); 

it.next(); // { value:1, done:false }  
it.next(); // { value:2, done:false } 
it.next(); // { value:3, done:false } 
it.next(); // { done:true },這個機制和 ES6 中發生器函數的語義相 關
  • 引用相似 iterator 的特殊屬性時要使用符號名,而不是符號包含的 值。此外,雖然看起來很像一個對象,可是 @@iterator 自己並非一個迭代 器對象,而是一個返回迭代器對象的函數——這點很是精妙而且很是重要。

和數組不一樣,普通的對象沒有內置的 @@iterator,因此沒法自動完成 for..of 遍歷。簡單來講,這樣作是爲了不影響將來的對象 類型。,咱們能夠給須要遍歷的對象定義@@iterator

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

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 定義一個無限迭代器,如產生隨機數等

var randoms = {    
    [Symbol.iterator]: function() {        
        return {            
            next: function() {               
                return { value: Math.random()
                       };            
            }       
        };     
    } }; 

var randoms_pool = []; 
for (var n of randoms) {   
    randoms_pool.push( n ); 
// 防止無限運行!    
    if (randoms_pool.length === 100) break; 
}

第四章 混合對象「類」

面向類的設計模式:實例化(instantiation),繼承(inheritance),(相對)多態(polymorphism)。

4.1 類理論

類理論包括:

  • 類(Vehicle類交通工具都包含的東西,Car類對通用Vehicle的特殊化)
  • 繼承
  • 實例化
  • 多態,父類的通用行爲能夠被子類用更特殊的行爲重寫(JS中不推薦)。

4.4.1 類設計模式

面向對象的設計模式有,迭代器模式、觀察者模式、工廠模式、單例模式等。

4.4.2 JavaScript中的類

JS中的類是構造函數與原型的結合,與其餘語言的類不一樣。

4.2 類的機制

4.2.1 建造

爲了得到真正能夠交互的對象,咱們必須按照類來建造一個東西,這個東西一般被稱爲實例。有須要的話,咱們能夠直接在實例上調用方法並訪問其全部公有數據屬性。

4.2.2 構造函數

類實例是由一個特殊的類方法構造的,這個方法名和類名相同,被稱爲構造函數。這個方法的任務是初始化實例所須要的全部信息及屬性。

function CoolGuy() {
    this.name = 'John';
}

構造函數須要用new來調用,這樣引擎纔會去構造一個新的類實例。

4.3 類的繼承

子類須要繼承父類的屬性與原型方法。

4.3.1 多態

子類能夠重寫父類方法。 子類重寫的方法在子類的prototype上,不會改變父類的prototype,只是子類在尋找方法時,會跟隨原型鏈,率先找處處於自身原型上的方法,從而調用。 ES6用super代指父類的構造函數,從而能讓子類經過這種方式找到父類的原型及其方法。

4.3.2 多重繼承

JS的繼承是單向的。 JS不支持多重繼承,所以使用混入模式實現多重繼承。

4.4 混入

JavaScript 中只有對象,並不存在能夠被實例化的「類」。一個對象並不會被複制到其餘對 象,它們會被關聯起來。JavaScript 開發者也想出了一個方法來 模擬類的複製行爲,這個方法就是混入。

4.4.1 顯式混入

JavaScript 中的函數沒法(用標準、可靠的方法)真正地複製,因此你只能複製對共享 函數對象的引用(函數就是對象;參見第 3 章)。若是你修改了共享的函數對象(好比 ignition()),好比添加了一個屬性,那 Vehicle 和 Car 都會受到影響

// 不覆蓋屬性
function mixinWithoutOverwrite(source, target) {
    for (var key in source){
        // 只在不存在的狀況下複製
        if( !(key in target) ){
            target[key] = source[key];
        }
    }
    return target;
}


//覆蓋屬性
//若是咱們是先進行復制而後對 Car 進行特殊化的話,就能夠跳過存在性檢查。不過這種方 法並很差用而且效率更低,因此不如第一種方法經常使用:
function mixinWithOverwrite(source, target) {
    for (var key in source){
        target[key] = source[key];    
    }

    return target;
}

//。從技術角度來講,函數實際上沒有 被複制,複製的是函數引用(對象數組)。

另外一種變體 — 是寄生式繼承,將父類的實例混入子類。它既是顯式的又是隱式的

function Super() {
    this.name = 'father';
}
Super.prototype.say = function() {
    console.log('Method from father');
}

function Sub() {
    // 父類實例
    var father = new Super();
    father.name = 'son';
    // 保存引用
    var extendedSay = father.say;
    // 多態重寫
    father.say = function() {
        extendedSay.call(this);
        console.log('Method from son');
    }
    return father;
}
// 調用時無需用new。是由於咱們沒有使用這個對象而是返回了咱們本身的 car 對象,所 以最初被建立的這個對象會被丟棄,所以能夠不使用 new 關鍵字調用 Car(),能夠避免建立並丟棄多餘的對象
var son = Sub();

4.4.2隱式混入

隱式混入其實就是把其餘對象的方法拿過來使用,使用call、apply、或bind,將其this指針指向自身。(儘可能避免使用這樣 的結構,以保證代碼的整潔和可維護性。)

var landLord = {
    name: 'himself',
    checkMoney: function() {
        console.log('Landlord is giving money to ' + this.name);
    },
}

var robinHood = {
    name: 'Robin Hood',
    rob: function() {
        // 把其餘對象的方法拿過來使用
        landLord.checkMoney.call(this);
    },
}

robinHood.rob(); //Landlord is giving money to Robin Hood

第五章 原型

5.1 [[Prototype]]

幾乎全部的對象在建立時 [[Prototype]] 屬性都會被賦予一個非空的值。

對於默認的Get操做來講,若是沒法在對象自己找到須要的屬性或方法,就會繼續訪問對象的Prototype鏈。

var another = {
    a:2
};
//建立一個關聯到anotherObject的對象
var myObj = Object.create(another);
// 能找到,可是在myObj.__proto__上找到
myObj.a; //2
// Object.create(..)會建立一個對象並把這個對象的 [[Prototype]] 關聯到指定的對象。
  • 使用for...in遍歷對象的原理和查找原型鏈類似。
  • 使用 in 操做符來檢查屬性在對象 中是否存在時,一樣會查找對象的整條原型鏈(不管屬性是否可枚舉)

5.1.1 Object.prototype

通常原型鏈都會最終指向Object.prototype,而Object.prototype的__proto__爲null。

5.1.2 屬性設置和屏蔽

  • 若是原型鏈上找不到某個屬性,該屬性就會被直接添加到調用對象上。
  • 若是屬性既出如今對象中,也出如今其原型鏈中,那麼對象中的屬性就會屏蔽原型鏈上層全部的同名屬性。 優先順序爲;對象實例屬性 > 下層原型 > 上層原型。
  • Set的狀況則更爲複雜,好比myObj自身沒有foo,但myObj.foo = 'bar'的狀況:
    1. 若是原型鏈上層存在名爲foo的屬性,且能夠寫入,那麼就會直接在myObj中添加一個名爲foo的新屬性,而且賦值進去。
    1. 若是原型鏈上層存在foo,但它是隻讀,那麼就沒法進行修改,非嚴格模式下被忽略,嚴格模式下拋出錯誤。(這樣作主要是爲了模擬類屬性的繼承。)
    1. 若是原型鏈上層存在foo,且是一個setter,那麼就會調用這個setter。此時foo不會添加到myObj,也不會改變這個setter。
      (若是你但願在第二種和第三種狀況下也屏蔽 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++; // 隱式屏蔽!

    myObject.a; // 3
    myObject.hasOwnProperty( "a" ); // true

  • ++ 操做至關於 myObject.a = myObject.a + 1。所以 ++ 操做首先會經過 [[Prototype]] 查找屬性 a 並從 anotherObject.a 獲取當前屬性值 2,而後給這個值加 1,接着用 [[Put]] 將值 3 賦給 myObject 中新建的屏蔽屬性 a,天吶!

5.2 類

5.2.1 類函數

全部函數默認都會擁有一個名爲prototype的公有且不可枚舉屬性,它其實指向另外一個對象。 同一個構造函數,及其構造的實例,它們對該函數prototype的引用其實指向同一段內存地址,修改這個prototype的屬性及方法,會影響到這個構造函數及其全部實例。

5.5.2 構造函數

prototype默認有一個公有且不可枚舉的屬性constructor,這個屬性引用的是對象關聯的函數。

function foo() {
    // ...
}
foo.prototype.constructor === foo; // true
var a = new foo();
// 並非a有這個屬性,它是在原型鏈上找到的
a.constructor === foo; // true

須要注意的是,構造函數自己仍是函數,new操做符只是將其調用方式變成「構造函數調用」,本質上仍是須要去執行它。

在 JavaScript 中對於「構造函數」最準確的解釋是,全部帶 new 的函數調用。

5.2.3 技術

prototype中的constructor引用是很是不可靠,且不安全的,要避免使用。.constructor 並非一個不可變屬性。它是不可枚舉的,可是它的值 是可寫的(能夠被修改)。

實例對象 並無 .constructor 屬性,因此它會委託__proto__鏈上的 Foo. prototype。

實際上,Foo 和你程序中的其餘函數沒有任何區別。函數自己並非構造函數,然而,當 你在普通的函數調用前面加上 new 關鍵字以後,就會把這個函數調用變成一個「構造函數 調用」。

new 會劫持全部普通函數並用構造對象的形式來調用它。

5.3 (原型)繼承

要建立一個合適的關聯對象,咱們必須使用 Object.create(..) 而不是使用具備副 做用的 Foo(..)。會建立一個新對象,再賦予當前對象

bar = Object.create( foo ); 中

Object.create(..),會建立一個新對象給bar,可是此時bar中並無內容,仍是指向的foo,能夠引用foo中的屬性;當foo中參數被修改時,bar的引用也會被修改;

可是當bar重寫屬性時,在當前實例中建立這個屬性,重寫的值不會改變foo的值!!!此後foo再次改變此屬性不會再影響bar的屬性值

ES6 添加了輔助函數 Object.setPrototypeOf(..),能夠用標準而且可靠的方法來修 改關聯。

// ES6 以前須要拋棄默認的 
Bar.prototype Bar.ptototype = Object.create( Foo.prototype ); 
 
// ES6 開始能夠直接修改現有的
Bar.prototype Object.setPrototypeOf( Bar.prototype, Foo.prototype )

a instanceof Foo; // true
instanceof 操做符的左操做數是一個普通的對象,右操做數是一個函數。instanceof 回答 的問題是:在 a 的整條 [[Prototype]] 鏈中是否有指向 Foo.prototype 的對象?

反過來操做就是:Foo.prototype.isPrototypeOf(a):foo是否出如今a的原型鏈中

//ES5
function Super(){
    this.name = 'father';
}
Super.prototype.say = function() {
    console.log('I am ' + this.name);
};

function Sub(){
    // 執行父類構造函數,得到屬性
    Super();
    // 添加或覆蓋屬性
    this.name = 'son';
}
// 造成原型鏈
Sub.prototype = Object.create(Super.prototype);
// 修復constructor指向
Sub.prototype.constructor = Sub;

// 或者更直接的原型鏈
Sub.prototype.__proto__ = Super.prototype;
//ES6提供一種新的操做方式
Object.setPrototypeOf(Sub.prototype, Super.prototype);


// ES6
class A {
    constructor(){
        this.name = 'father'
    }
    say() {
        console.log('Method from father: I am ' + this.name);
    }
}
class B extends A {
    constructor(){
        super();
        this.name = 'son'
    }
    say() {
        console.log(`Method from son: I am ${this.name}.`);
        super.say();
    }
}

另外一個須要注意的是非標準的proto,它實際上是一套getter/setter。

Object.defineProperty(Object.prototype, "__proto__", {
    get: function() {
        return Object.getPrototypeOf(this);
    },
    set: function(o) {
        Object.setPrototypeOf(this, o);
        return o;
    },
});

5.4 對象關聯

5.4.1 建立關聯

//Object.create()的polyfill
if(!Object.create){
    Object.create = function(o) {
        function F(){}
        F.prototype = o;
        return new F();
    };
}

5.4.2 關聯關係是備用?

原型的實現應當遵循委託設計模式,API在原型鏈上出現的位置要參考現實中的狀況。

第六章 行爲委託

6.1 面向委託的設計

6.1.1 類理論

類設計模式鼓勵你在繼承時使用方法重寫和多態。許多行爲能夠先抽象到父類,而後再用子類進行特殊化。

6.1.2 委託理論

首先定義對象,而不是類。經過Object.create()來建立委託對象,賦值給另外一個對象,從而讓該對象出如今另外一個對象的原型鏈上。 好比var bar = Object.create(foo);,使得bar.__proto__===foo,從而bar能夠經過原型鏈得到foo的全部屬性和方法。 這種設計模式被稱爲對象關聯。委託行爲意味着某些對象在找不到屬性或者方法引用時,會把這個請求委託給另外一個對象。

須要注意的是,禁止兩個對象互相委託,不然當引用一個二者都不存在的屬性或方法,會產生無限遞歸的循環。

function Foo() {} 
var a1 = new Foo(); 
a1; // Foo {} (chrome中) ; Object {} (Firefox中)
//「{} 是一個空對象,由名爲 Foo 的函數構造」 ,由於 Chrome 會動態跟蹤並把 實際執行構造過程的函數名看成一個內置屬性,可是其餘瀏覽器並不會跟蹤這些額外的信息

6.1.3 比較思惟模型

對象關聯風格相對於類風格更爲簡潔,由於它只關注對象之間的關聯關係。

Foo = {
    init: function(who) {
        this.me = who;
    },
    identify: function() {
        return 'I am ' + this.me;
    },
}
Bar = Object.create(Foo);
Bar.speak = function() {
    console.log('Hello, ' + this.identify() + '.');
};

var b1 = Object.create(Bar);
var b2 = Object.create(Bar);
// 從b1.__proto__.__proto__上獲得init方法
b1.init('b1');
b2.init('b2');

// 從b1.__proto__上獲得Bar的spaak方法
// 從b1.__proto__.__proto__上獲得identify方法
b1.speak(); // Hello, b1.
b2.speak(); // Hello, b2.

6.2.2 委託控件對象

委託模式能夠防止上一節中不太合理的僞多態形式調用Widget.prototype.render.call(this, $where);。 同時init和setup兩個初始化函數更具語義表達能力,同時因爲將構建和初始化分開,使得初始化的時機變得更靈活,容許異步調用。 對象關聯能夠更好地支持關注分離原則,建立和初始化不須要合併爲一個步驟。

6.3 更簡潔的設計

對象關聯除了能讓代碼看起來更簡潔,更具擴展性,還能夠經過委託模式簡化代碼結構。

6.4 更好的語法

ES6中使用Object.setPrototypeOf(target, obj)的方式來簡化target = Object.create(obj)的寫法。

6.5 內省

內省就是檢查實例的類型。 instanceof實際上檢查的是目標對象與構造器原型的關係,對於經過委託互相關聯的對象(Foo與Bar互相關聯),可使用:

  • Foo.isPrototypeOf(Bar)
  • Object.getPrototypeOf(Bar) === Foo
相關文章
相關標籤/搜索