你不知道的JS(上冊)

你不知道的JS學習筆記

第一部分:做用域和閉包

第1章 做用域是什麼

1.1 編譯原理

儘管一般將JavaScript歸類爲「動態」或「解釋執行」語言,但事實上它是一門編譯語言。與傳統的編譯語言不一樣,它不是提早編譯的,編譯結果也不能在分佈式系統中進行移植。css

在傳統編譯語言的流程中,程序中一段源代碼在執行以前會盡力三個步驟,即「編譯」:算法

  • 分詞/詞法分析(Tokenizing/Lexing)

這個過程會將由字符組成的字符串分解成有意義的代碼塊,這些代碼塊被成爲詞法單元編程

var a = 2; 
//分解成 var、a、=、二、;。
//空格是否會被看成詞法單元,取決於空格在這門語言中是否具備意義。
複製代碼

分詞和詞法分析主要差別在於詞法單元的識別是經過有狀態仍是無狀態的方式進行的。設計模式

簡單的說,若是詞法單元生成器在判斷a是一個獨立的詞法單元仍是其餘詞法單元的一部分時, 調用的是有狀態的解析規則,那麼這個過程就被稱爲詞法分析數組

  • 解析/語法分析(Parsing)

這個過程是將詞法單元流(數組)轉換成一個由元素逐級嵌套所組成的表明了程序語法結構的樹。這個樹被稱爲「抽象語法樹」(Abstract Syntax Tree, AST)。瀏覽器

  • 代碼生成

將AST轉換爲可執行代碼的過程被稱爲代碼生成。安全

拋開具體細節,簡單來講就是有某種方法能夠將var a = 2;的AST轉化爲一組機器指令,用來建立一個叫做a的變量(包括分配內存等),並將一個值儲存a中。bash

JS引擎要複雜的多。例如,在語法分析和代碼生成階段有特定的步驟來對運行性能進行優化,包括對冗餘元素進行優化等。服務器

1.2 理解做用域

做用域:負責收集並維護由全部的標識符(變量)組成的一系列查詢,並實施一套很是嚴格的規則,肯定當前執行的代碼對這些標識符的訪問權限。閉包

變量的賦值操做會執行兩個動做,首先編譯器會在當前做用域中聲明一個變量(若是以前沒有聲明過),而後在運行時引擎會在做用域中查找該變量,若是可以找到就會對它賦值。

  • 編譯器

    • LHS查詢(目的是對變量賦值)
    • RHS查詢(獲取變量的值)

    當變量出如今賦值操做的左側時進行LHS查詢,出如今右側時進行RHS查詢。

1.3 做用域嵌套(由內到外查找)

1.4 異常

若是RHS查詢在全部嵌套的做用域中遍尋不到所需的變量,引擎就會拋出ReferenceError異常。

相較之下,當引擎執行LHS查詢時,若是在頂層(全局做用域)中也沒法找到目標變量,全局做用域中就會建立一個具備該名稱的變量,並將其返還給引擎,前提是程序運行在非「嚴格模式」下。(嚴格模式禁止自動或隱式地建立全局變量)

ReferenceError同做用域判別失敗相關,而TypeError則表明做用域判別成功了,可是對結果的操做時非法或不合理的。

第2章 詞法做用域

2.1 詞法階段

詞法做用域就是定義在詞法階段的做用域。

做用域查找會在找到第一個匹配的標識符時中止。在多層的嵌套做用域中能夠定義同名的標識符,這叫做「遮蔽效應」(內部的標識符遮蔽了外部的標識符)。

全局變量會自動稱爲全局對象(好比瀏覽器中的window對象)的屬性,所以能夠不直接經過全局對象的詞法名稱,而是間接的經過對全局對象屬性的引用來對其訪問。例如:

window.a
複製代碼

2.2 欺騙詞法

欺騙詞法做用域會致使性能降低。(不推薦使用)

- eval
- with
複製代碼

第3章 函數做用域和塊做用域

函數做用域的含義是指, 屬於這個函數的所有變量均可以在整個函數的範圍內使用及複用(事實上在嵌套的做用域中也可使用)。

3.1 隱藏內部實現(阻止對私有變量或私有函數的訪問)

隱藏變量和函數是從最小特權原則中引伸出來的,也叫最小受權或最小暴露原則。在軟件設計中,應該最小限度地暴露必要內容。而將其餘內容都「隱藏」起來,好比某個模塊或對象的API設計。

  • 規避衝突

    • 全局命名空間(用對象的屬性進行訪問)
    var MyReallyCoolLibrary = {
        awesome: "stuff",
        doSomething: function() {
            //...
        }
        doAnotherThing: function() {
            //...
        }
    };
    複製代碼
    • 模塊管理

3.2 函數做用域

區分函數聲明和表達式最簡單的方法是看function關鍵字出如今聲明中的位置(不只僅是一行代碼,而是整個聲明中的位置)。若是function是聲明中的第一個詞,那麼就是一個函數聲明,不然就是一個函數表達式。

匿名函數的幾個缺點:

  1. 匿名函數在棧追蹤中不會顯示出有意義的函數名,使得調試很困難。
  2. 若是沒有函數名,當函數須要引用自身時只能使用已通過期的arguments.callee, 好比在遞歸中。另外一個函數須要引用自身的例子,是在事件觸發後事件監聽器須要解綁自身。
  3. 匿名函數省略了對於代碼可讀性 / 可理解性很重要的函數名。一個描述性的名稱可讓代碼不言自明。

3.3 塊做用域

塊做用域時一個用來對以前的最小受權原則進行擴展的工具,將代碼從函數中隱藏信息擴展爲在塊中隱藏信息。

3.3.1 with
3.3.2 try/catch
try{
    undefined(); //執行一個非法操做來強制製造一個異常
}catch( err ){
    console.log( err ); // 可以正常執行
}
console.log( err ); //ReferenceError: err not found

複製代碼
3.3.3 let

只要聲明是有效的,在聲明中的任意位置均可以使用{...}括號來爲let建立一個用於綁定的塊。

{
    console.log( bar ); //ReferenceError!
    let bar = 2;
}
複製代碼
    1. 垃圾收集
function process(data) {
    // 在這裏作點有趣的事情
}

var someReallyBigData = {...};

process( someReallyBigData );

var btn = document.getElementById( "my_button" );

btn.addEventListener("click", function click(evt) {
    console.log("button clicked");
}, /*capturingPhase=*/false);
複製代碼
    1. let循環
3.3.4 const

第四章 提高

任何聲明在某個做用域內的變量,都將附屬於這個做用域。

4.1 編譯器的正確思考思路

變量和函數在內的全部聲明都會在任何代碼被執行前首先被處理。

var a = 2;不是一個聲明,在JavaScript實際上會將其當作兩個聲明:

var a;a = 2;。第一個定義聲明是在編譯階段進行的。第二個賦值聲明會留在原地等待執行階段。

只有聲明自己會被提高,而賦值或其餘運行邏輯會留在原地。

var a; //編譯階段
a = 2; //執行
console.log( a );
複製代碼
foo(); //TypeError 類型(執行時)
bar(); //ReferenceError 引用(未定義)

var foo = function bar() {
    //...
}
複製代碼

提高以後 =>

var foo;
foo();
bar();
foo = function () {
    var bar = ...self...;
    //...
}
複製代碼

4.2 函數優先

函數聲明和變量聲明都會被提高,函數首先被提高,而後纔是變量。

foo(); //1 !!!
var foo;
function foo() {
    console.log( 1 );
}

foo = function() {
    console.log( 2 );
}
複製代碼

一個普通塊內部的函數聲明一般會被提高到所在做用域的頂部,這個過程不會像下面的代碼暗示的那樣能夠被條件判斷所控制。

foo(); //TypeError: foo is not a function
var a = true;
if (a) {
    function foo() { console.log("a"); }
}else{
    function foo() { console.log("b"); }
}
複製代碼

應該儘量避免在塊內部聲明函數。

第5章 做用域閉包

理解閉包能夠看做是某種意義上的重生

5.1 實質

當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包,即便函數是在當前詞法做用域以外執行。

function foo() {
    var a = 2;
    
    function bar() {
        console.log( a );
    }
    
    return bar
}

var baz = foo();
baz(); //2, 唔,這就是閉包的效果 
複製代碼

閉包的神奇之處在於能夠阻止引擎垃圾回收器對某內部做用域進行回收。

//直接傳遞函數
function foo() {
    var a = 2;
    
    function baz() {
        console.log( a ); //2
    }
    
    bar( baz );
}

function bar(fn) {
    fn(); //閉包, 外部調用baz,能夠訪問a
}

foo();
複製代碼
//間接傳遞函數
var fn;
function foo() {
    var a = 2;
    
    function baz() {
        console.log( a );
    }
    
    fn = baz; //將baz分配給全局變量
}

function bar() {
    fn(); //閉包, 外部調用baz,能夠訪問a
}

foo();
bar(); //2
複製代碼

5.2 閉包使用場景

function wait(message) {
    
    setTimeout( function timer() {
        console.log( message );
    }, 1000)
}

wait( "Hello, closure!" );
複製代碼

在引擎內部,內置的工具函數setTimeout持有對一個參數的引用,這個參數也許叫做fnfunc,或者其餘類型的名字。引擎會調用這個函數,在例子中就是內部的Timer函數,而詞法做用域在這個過程當中保持完整。

在定時器,事件監聽器,Ajax請求,跨窗口通訊,Web Workers或者任何其餘的異步(或者同步)任務中,只要使用了回調函數,實際上就是在使用閉包。

閉包就是「一塊特定的做用域」 --- 我的理解

5.3 循環和閉包

for (var i=1; i<=5; i++) {
    setTimeout( function timer()  {
        console.log( i );
    }, i*1000 );
}
複製代碼

咱們試圖在每一個迭代時都會給本身「捕獲」一個i的副本。但根據做用域的工做原理,實際狀況是儘管循環中的五個函數是在各個迭代中分別定義的,可是它們都被封閉在一個共享的全局做用域中,所以實際上只有一個i

IIFE

for (var i=1; i<=5; i++) {
    (function(j) {
        setTimeout( function timer() {
            console.log( j );
        }, i*1000 )
    })(i);
}
複製代碼

let

for (let i=1; i<=5; i++) {
    setTimeout( function timer() {
        console.log( j );
    }, i*1000 )
}
複製代碼

5.4 模塊

5.4.1 模塊介紹
function CoolModule() {
    var something = "cool";
    var another = [1, 2, 3];
    function doSomething() {
        console.log( something );
    }
    
    function doAnother() {
        console.log( another.join( "!" ) );
    }
    
    return {
        doSomething: doSomething,
        doAnother: doAnother
    }
}

var foo = CoolModule();

foo.doSomething(); //cool
foo.doAnother(); //1 ! 2 ! 3
複製代碼

這個模式在Javascript中稱爲模塊。

從模塊中返回一個實際的對象並非必須的,也能夠直接返回一個內部函數。jQuery就是一個很好的例子,jQuery$標識符就是jQuery模塊的公用API,但它們自己都是函數(因爲函數也是對象,它們自己也能夠擁有屬性)。

模塊模式須要具有兩個條件。

  1. 必須有外部的封閉函數,該函數必須至少被調用一次(每次調用都會建立一個新的模塊實例)。
  2. 封閉函數必須返回至少一個內部函數,這樣內部函數才能在私有做用域中造成閉包,而且能夠訪問或者修改私有的狀態。
單例模式
var foo = (function CoolModule(id) {
    function change() {
        //修改公共API
        publicAPI.identify = identify2;
    }
    
    function identify1() {
        console.log( id );
    }
    
    function identify2() {
        console.log( id.toUpperCase() );
    }
    var publicAPI = {
        change: change,
        identify: identify1
    };
    
    return publicAPI;
})(" foo module ");

foo.identify(); //foo module
foo.change(); // 1 ! 2 ! 3
foo.identify(); //FOO MODULE
複製代碼
5.4.2 現代的模塊機制
5.4.3 將來的模塊機制(ES6)
//bar,js

function hello(who) {
    return "Let me introduce: " + who;
}

export hello;
複製代碼
//foo.js
//僅從「bar」模塊導入hello()
import hello from 'bar';

var hungry = 'hippo';

function awesome() {
    console.log( 
        hello ( hungry ).toUpperCase();
    )
}

export awesome;
複製代碼
//baz.js
//導入完整的「foo」和「bar」模塊
module foo from "foo";
module bar from "bar";

console.log(
    bar.hello( 'rhino' )
); //Let me introduce: rhino

foo.awesome(); //LET ME INTRODUCE: HIPPO
複製代碼

第二部分:this和對象原型

第1章 關於this

任何足夠先進的技術都和魔法無異。--- Arthur C.Clarke

使用this能夠自動引用合適的上下文對象,而不須要顯式傳遞上下文對象,這樣可讓代碼更簡潔。

1.1 關於this的誤解:

  • 指向自身
function foo() {
    console.log( "foo: " + num );
    
    //記錄count被調用的次數
    this.count++; //無心中建立了一個全局變量,它的值爲NaN, this(默認)指向全局。
}

foo.count = 0;

var i;

for(i=0; i<10; i++) {
    if(i > 5) {
        foo( i );
    }
}
// foo: 6
// foo: 7
// foo: 8
// f00: 9

console..log(foo.count); // 0 爲何是0?
複製代碼

改進:

function foo() {
    console.log( "foo: " + num );
    
    //記錄count被調用的次數
    //注意,在當前的調用方式下(參見下方代碼),this確實指向foo
}

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
// f00: 9

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和詞法做用域的查找混合使用時,必定要提醒本身,這是沒法實現的。

1.2 總結

this其實是在函數被調用時發生的綁定,它指向什麼徹底取決於函數在哪裏被調用。

第2章 this全面解析

2.1 調用位置---分析調用棧

利用瀏覽器的調式工具

2.2 綁定規則

  • 默認綁定(獨立函數調用)
function foo() {
    console.log( this.a );
}

var a = 2;
// 無任何修飾調用,默認綁定[非嚴格模式]
foo(); //2

複製代碼

嚴格模式

function foo() {
    "use strict";
    
    console.log( this.a );
}

var a = 2;
// 嚴格模式
foo(); // TypeError: this is not defined

複製代碼

雖然this的綁定規則徹底取決於調用位置,可是隻有foo()運行在非strict mode下時,默認綁定才能綁定到全局對象;在嚴格模式下調用foo()則不影響默認綁定:

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

var a = 2;

(function(){
    "use strict";
    
    foo();//2
})();
複製代碼
  • 隱式綁定

考慮的規則: 調用的位置是否具備上下文對象。

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

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

obj.foo(); //2 函數被調用時obj對象「擁有」或者「包含」函數引用。
複製代碼

對象屬性引用鏈中只有上一層或者說最後一層在調用位置中起做用。

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

var obj2 = {
    a: 42,
    foo: foo
};

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

obj1.obj2.foo(); //42
複製代碼

隱式丟失---隱式綁定的函數會丟失綁定對象,會應用默認綁定,從而幫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"
複製代碼

發生在傳入回調函數的狀況(更常見,更微妙【更變態】):

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"

//把函數傳入語言內置的函數而不是傳入你本身聲明的函數,結果同樣。好比傳入setTimeout
複製代碼
  • 顯示綁定

call, applybind

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

var obj = {
    a: 2
};

foo.call( obj ); // 2
複製代碼

若是你傳入額餘個原始值(字符串類型、布爾類型或者數字類型)來當作this的綁定對象,這個原始值會被轉換成它的對象形式(也就是new String(..)new Boolean(..)或者new Number(..))。這一般被成爲裝箱。

1. 硬綁定 call, apply和bind

2. API調用的上下文
```
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實現了現實綁定
```
複製代碼
  • new綁定

JavaScript中的構造函數: 在JavaScript中,構造函數只是一些使用new操做符時被調用的函數。它們並不會屬於某個類,也不會實例化一個類。實際上,它們甚至都不能說是一種特殊的函數類型,它們只是被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
複製代碼

2.3 優先級

判斷this

  1. 函數是否在new中調用(new綁定)?若是是,this綁定的是新建立的對象;
  2. 函數是否經過callapply(顯示綁定)或硬綁定調用?若是是,this綁定的是指定的對象;
  3. 函數是否在某個上下文對象中調用(隱式綁定)?若是是,this綁定的是哪一個上下文對象;
  4. 若是都不是,使用默認綁定。嚴格模式,綁定到undefined,不然綁定到全局對象。

2.4 綁定例外

  • 被忽略的this

若是你把null或者undefined做爲this的綁定對象傳入call, apply或者bind,這些值在調用時會被忽略,實際應用的是默認綁定規則。

老是使用null來忽略this綁定可能產生一些反作用。若是這個函數確實使用了this(好比第三方庫中的一個函數),那默認綁定規則會把this綁定了全局對象(在瀏覽器中這個對象是window),這將致使不可預計的後果(好比修改全局對象)。

更安全的this(不對全局對象產生影響)

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
複製代碼
  • 間接引用
function foo() {
    console.log( this.a );
}

var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };

o.foo(); //3 隱式綁定
(p.foo = 0.foo)(); //2 默認綁定
複製代碼

**注意:**對於默認綁定來講,決定this綁定對象的並非調用位置是否處於嚴格模式,而是函數體是否處於嚴格模式。

  • 軟綁定

基於硬綁定的問題:硬綁定會大大下降函數的靈活性,使用硬綁定以後就沒法使用隱式綁定或者顯示綁定來修改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 : this,
                curried.concat.apply( curried, arguments );
            );
        };
        bound.prototype = Object.create( fn.prototype );
        return bound;
    }
}
複製代碼

2.5 this詞法

箭頭函數不使用this的四種標準規則,而是根據外層(函數或者全局)做用域來決定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
複製代碼

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

建議:

  1. 只使用詞法做用域並徹底拋棄錯誤this風格的代碼;
  2. 徹底採用this風格,在必要時使用bind(..),儘可能避免使用self = this和箭頭函數。

第3章 對象

3.1 語法和類型

  1. 對象能夠經過兩種形式定義: 聲明(文字)形式和構造形式。

聲明(文字)形式:

var myObj = {
    key: value
    //...
}
複製代碼

構造形式(少用):

var myObj = new Object();
myObj.key = value;
複製代碼
  1. Javascript中一共有六種主要類型:
  • string
  • number
  • boolean
  • null
  • undefined
  • object

注意: 以上簡單類型自己並非對象。null有時會被看成一種對象類型,可是這其實只是語言自己的一個bug,即對null執行typeof null會返回字符串'object'。實際上,null自己事基本類型(解釋)。

不一樣的對象在底層都表示爲二進制,在JavaScript中二進制前三位都爲0的話會被判斷爲object類型,null的二進制表示全爲0,天然前三位也是0,因此執行typeof會返回'object'

  1. 內置對象:
  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

在Javascript中,以上內置對象實際上只是一些內置函數,能夠看成構造函數來使用。

3.2 對象內容

存儲在對象容器內部的事這些屬性的名稱,它們就像指針(從技術角度來講就是引用)同樣(),指向這些值真正的存儲位置()。

  • 屬性和方法:

「函數」和「方法」在Javascript中是能夠互換的。即便你在對象的文字形式中聲明一個函數表達式,這個函數也不會「屬於」這個對象 --- 它們只是對於相同函數對象的多個引用。

  • 數組:
var myArray = [ "foo", 42, "bar" ];
myArray.baz = 'baz';
myArray.length; // 3
myArray.baz; // 'baz' ---添加了命名屬性,可是數組的length並無發生變化。
複製代碼

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

var myArray = [ "foo", 42, "bar" ];
myArray['3'] = 'baz';
myArray.length; // 4
myArray[3]; // 'baz';
複製代碼
  • 複製對象

深複製:

對於JSON安全(也就是說能夠被序列化爲一個JSON字符串而且能夠根據這個字符串解析出一個結構和值徹底同樣的對象)的對象來講,能夠經過如下方式進行復制:

var newObj = JSON.parse( JSON.stringify( someObj ) )
複製代碼

淺複製:

使用ES6方法Object.assign()

var newObj = Object.assign( {}, newObject )
複製代碼

**注意:**因爲Object.assign(..)就是使用=操做符來賦值,因此源對象屬性的一些特性(好比writable)不會被賦值到目標對象。

  • 屬性描述符writable, configurable, enumerable.

    • writable嚴格模式與非嚴格模式

    • configurable修改爲false是單向操做,沒法撤銷。

    • 即使屬性是configurable: false,咱們仍是能夠把writable的狀態由true改成false,可是沒法由false改成true

    • 不要把delete看做一個釋放內存的工具,它就是一個刪除對象的操做而已。

  • 不變性

    • 對象常量
    • 禁止擴展
    • 密封
    • 凍結
  • [[Get]]/[[Put]]

var myObject = {
    a: 2
}

myObject.a; //2
複製代碼

在語言規範中, myObject.amyObject上實際上實現了[[Get]]操做(有點像函數調用:[[Get]]())。對象默認的內置[[Get]]操做首先在對象中查找是否有名稱相同的屬性,若是找到就返回這個屬性的值。若是沒有找到這個屬性,按照[[Get]]算法的定義會執行另一種很是重要的行爲 --- 遍歷可能存在的原型鏈。

  • Getter/Setter
var myObject = {
    // 給a定義一個getter
    get a() {
        return 2;
    };
}

Object.defineProperty(
    myObejct, // 目標對象
    'b', //屬性名
    {
        // 描述符
        // 給b設置一個getter
        get: function() {
            return this.a * 2
        }
        // 確保b出如今對象的屬性列表中
        enumerable: true
    }
)

myObject.a; // 2
myObject.b; // 4
複製代碼
  • 存在性

in操做符會檢查屬性是否在對象及其[[Prototype]]原型鏈中。相比之下,hasOwnProperty(..)以後檢查屬性是否在myObject對象中,不會檢查原型鏈。

對象可能沒有鏈接到Object.prototype,直接使用myObject.hasOwnProperty(..)會失敗,能夠採用一種更增強硬的方法來進行判斷:Object.prototype.hasOwnProperty.call(myObejct, "a"),它解壓基礎的hasOwnProperty(..)方法並把它顯示綁定到myObject上。

4 in [2, 4, 6]; //fasle
//[2, 4, 6]這個數組中包含的屬性名是0,1,2,沒有4
複製代碼
-數組 <-- for循環
-對象 <-- for..in
複製代碼
var myObject = {};

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

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

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

Object.keys( myObject ); // ["a"]
Object.getOwnPropertyNames( myObject ); // ["a", "b"]

複製代碼

propertyIsEnumerable(..)會檢查給定的屬性名是否直接存在於對象中(而不是在原型鏈上)而且知足enumerable:true

Object.keys(..)會返回一個數組,包含全部可枚舉屬性,Obejct.getOwnPropertyNames(..)會返回一個數組,包含全部屬性,不管它們是否可枚舉。

in和hhasOwnProperty(..)的區別在因而否查找[[prototype]]鏈,然而,Object.keys(..)Object.getOwnPropertyNames(..)都只會查找對象直接包含的屬性。

3.3 遍歷

forEach(..)

every(..)

some(..)

for..of循環語法

和數組不一樣,普通的對象沒有內置的@@iterator,因此沒法自動完成for..of循環。

==> 給任何想遍歷的對象定義@@iterator

var object = {
    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)
                };
            }
        };
    }
});

//手動遍歷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 (var v of myObject){
    console.log( V );
}
// 2
// 3
複製代碼

第4章 混合對象「類」

4.1 類理論

類/繼承描述了一種代碼的組織結構方式 --- 一種在軟件中對真實世界中問題領域的建模方法。

面向對象編程強調的是數據和操做數據的行爲本質上是互相關聯的,所以好的設計是把數據以及和它相關的行爲打包。

4.2 類的機制

  • 構造函數

類實例是由一個特殊的類方法構造的,在各個方法一般和類名相同,被成爲構造函數。

//類
class CoolGuy {
    specialTrick = nothing
    
    CoolGuy( trick ) {
        specialTrick = trick
    } //類方法,構造函數
    
    showOff() {
        output( "Here's my trick: ", specialTrick )
    }
}

//實例化一個對象
Joe = new CoolGuy("jumping rope")
Joe.showOff() // Here's my trick: jumping rope 複製代碼

4.3 類的繼承

  • 多態

    相對多態: 之因此說「相對」是由於咱們並不會定義想要訪問的絕對繼承層次(或者說類),而是使用相對引用「查找上一層」。

    多態的另外一個方面是,在繼承鏈的不一樣層次中一個方法名能夠被屢次定義,當強調方法時會自動選擇合適的定義。

    在傳統的面向類的語言中,構造函數是屬於類的,而Javascript中剛好相反,實際上「類」是屬於構造函數的。(相似Foo.prototype...這樣的類型引用)。因爲JavaScript中父類和子類的關係只存在與二者構造函數對應的.prototype對象中,所以它們的構造函數直接並不存在直接聯繫,從而沒法簡單地實現二者的相對引用。

    多態並不表示子類和父類有關聯,子類獲得的只是父類的一份副本。類的繼承其實就是複製。

  • 多重繼承(繼承多個父類)

注意: 上面說類的繼承其實就是複製是針對其餘傳統語言來講的,而Javascript在繼承時一個對象並不會被複制到其它對象,只是關聯起來。

4.4 混入

在繼承或者實例化時,Javascript的對象機制不會自動執行復制行爲。簡單來講,JavaScript中只有對象,並不存在能夠被實例化的「類」。一個對象並不會被複制到其餘對象,它們會被關聯起來。

混入的意義在於模擬類的複製行爲,分爲顯式混入和隱式混入。

  • 顯式混入
function mixin( soureceObj, targetObj ) {
    for ( var key in sourceObj){
        // 只會在不存在的狀況下複製
        if( !key in targetObj ) {
            targetObj[key] = sourceObj[key];
        }
    }
    return targetObj;
}

var Vehicle = {
    engines: 1,
    ignition: function() {
        console.log( "Turning on my engine." );
    },
    drive: function() {
        this.ignition();
        console.log( "Steering and moving froward!" );
    }
};

var Car = mixin( Vehicle, {
    wheels: 4,
    drive: function() {
        Vehicle.drive.call( this ); //顯式多態
        console.log(
            "Rolling on all " + this.wheels + "wheels!"
        );
    }
} );
複製代碼

寄生繼承

function Vehicle() {
    this.engines = 1;
}
Vehicle.prototype.ignition = function() {
    console.log("Turning on my engine.");
};
Vehicle.prototype.drive = function() {
    this.ignition();
    console.log("Steering and moving forward");
};

//"寄生類" Car
function Car() {
    // 首先,car是一個Vehicle
    var car = new Vehicle();
    
    //接着咱們對car進行定製
    car.wheels = 4;
    
    //保存到Vehicle::drive()的特殊引用
    var vehDrive = car.drive;
    
    //重寫Vehicle::drive
    car.drive = function() {
        vehDrive.call( this );
        console.log("rolling on all" + this.wheels + "wheels!");
    };
    return car;
}

var myCar = new Car();

myCar.drive();
複製代碼
  • 隱式混入
var something = {
    cool: function() {
        this.greeting = "Hello world";
        this.count = this.count ? this.count + 1 : 1;
    }
};

something.cool();
something.greeting; //"Hello world"
something.count; //1

var Another = {
    cool: function() {
        //隱式把Something混入Another
        Something.cool.call( this );
    }
};

Another.cool();
Another.greeting; //"Hello world"
Another.count; // 1 (count 不是共享狀態)
複製代碼

第5章 原型

5.1 [[Prototype]]

使用for..in 遍歷對象時原理和查找[[Prototype]]相似,任何能夠經過原型鏈訪問到的屬性都會被枚舉。使用in操做符來檢查屬性咋對象中是否存在時,一樣會查找整條原型鏈。

  • 全部普通[[prototype]]鏈最終都會指向內置的Object.prototype
  • 屬性設置與屏蔽 若是向[[prototype]]鏈上層已經存在的屬性([[Put]])賦值,不必定會觸發屏蔽。須要觀察[[prototype]]鏈上層的該屬性是否標記爲只讀(writable:false),或者[[prototype]]鏈上層存在該屬性,而且它是一個setter,那就必定會調用setter。屬性爲只讀和爲setter的狀況下都不能觸發屏蔽。(儘可能避免使用屏蔽)
<!--注意隱式屏蔽-->
var anotherObject = {
    a: 2
}

var myObject = Object.create( anotherObject );

anotherObject.a; // 2
myObject.a; // 2

anotherObject.hasOwnProperty( "a" ); //true
myObject.hasOwnProperty( "a" ); //fasle

myObject.a++; //隱式屏蔽,其實等價於myObject.a = myObject.a + 1;
anotherObject.a; // 2
myObject.a; // 3

myObject.hasOwnProperty( "a" ); //true
複製代碼

5.2 「類」

JavaScript 只有對象。

  • 類函數
function Foo() {
    //....
}

var a = new Foo();

Object.getPrototypeOf( a ) === Foo.prototype; //true
複製代碼

Foo的原型-Foo.prototype,經過調用new Foo()建立的每一個對象將最終被[[Prototype]]連接到這個「Foo.prototype」對象。

在面向類的語言中,類能夠被複制屢次,就像用模具製做東西同樣。而JavaScript沒有相似的複製機制,不能建立一個類的多個實例,只能建立多個對象,它們[[Prototype]]關聯的是同一個對象。new Foo只是讓兩個對象互相關聯。

委託能夠更加準確的描述JavaScript的對象關聯機制。

  • "構造函數"
function Foo() {
    //...
}

Foo.prototype.constructor === Foo; //true
var a = new Foo();
a.constructor === Foo; //true
複製代碼

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

函數不是構造函數,可是當且僅當使用new時,函數調用會變成「構造函數調用」。

Foo.prototype.constructor屬性只是Foo函數在聲明時的默認屬性。若是你建立一個新對象並替換了函數默認的.prototype對象引用,那麼新對象並不會自動得到.constructor屬性。

function Foo() { /*..*/ }
Foo.prototype = { /*..*/ }; //建立一個新原型對象

var a1 = new Foo();
a1.constructor === Foo; //false
a1.constructor === object; //true
複製代碼

手動修復.constructor屬性

Object.defineProperty( Foo.prototype, "constructor", {
    enumerable: false,
    writable: true,
    configurable: true,
    value: Foo //讓.constructor指向Foo
})
複製代碼

constructor並不表示(對象)被(它)構造。

5.3 (原型)繼承

function Foo() {
    this.name = name;
}

Foo.prototype.myName = function() {
    return this.name;
}

function Bar(name, label){
    Foo.call( this, name );
    this.label = label;
}

//建立一個新的Bar.prototype對象並關聯到Foo.prototype
Bar.prototype = Object.create( Foo.prototype );

//注意,如今沒有Bar.prototype.constructor了
//若是須要,能夠手動修復

Bar.prototype.myLabel = function() {
    return this.label;
};

var a = new Bar( "a", "obj a" );

a.myName(); //"a"
a.myLabel(); //"obj a"
複製代碼

上述代碼的核心部分:調用Object.create(..)會建立一個「新」對象並把新對象內部的[[Prototype]]關聯到你指定的對象。(這裏是Foo.prototype), "建立一個新的Bar.prototype對象並吧它關聯到Foo.prototype"

兩種把Bar.prototype關聯到Foo.prototype的方法

//ES6以前須要拋棄默認的Bar.prototype
Bar.prototype = Object.create( Foo.prototype );

//ES6開始能夠直接修改現有的Bar.prototype
Obejct.setPrototypeOf( Bar.prototype, Foo.prototype )
複製代碼
  • 檢查「類」關係

在傳統的面向類環境中檢查一個實例(JavaScript中的對象)的繼承祖先(JavaScript中的委託關聯)一般被稱爲內省(或者反射)。

instanceof --- 在a的整條[[Prototype]]鏈中是否有Foo.prototype指向的對象?

isPrototypeOf --- 在a的整條[[Prototype]]鏈中是否出現過Foo.prototype?

b.isPrototypeOf(c); //b是否出如今c的[[Prototype]]鏈中  
//這個方法並不須要使用函數「類」,它直接使用b和c之間的對象引用來判斷它們的關係。
複製代碼
//直接獲取一個對象的鏈
Object.getPrototypeOf( a );

Object.getPrototypeOf( a ) === Foo.prototype; // true

//絕大多數瀏覽器支持的一種訪問內部[[Prototype]]屬性
a._proto_ === Foo.prototype; //true
複製代碼

._proto_的實現(笨蛋「proto」)

Object.defineProperty( Object.prototype, "_proto_", {
    get: function() {
        return Object.getPrototypeOf( this );
    },
    set: fucntion() {
        // ES6中的setPrototypeOf(...)
        Obejct.setPrototypeOf( this, o );
        return o;
    }
})
複製代碼

5.4 對象關聯

Object.create(null)這個對象沒有原型鏈,因此instanceOf操做符沒法進行判斷,總返回false,不受原型鏈的干擾,所以很是適合用來存儲數據。

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

複製代碼

委託設計模式

「委託」是一個更合適的術語,由於對象直接的關係不是複製而是委託。

第6章 行爲委託

JavaScript中這個機制的本質就是對象直接的關聯關係。

6.1 面向委託的設計

試着把思路從類和繼承裝換到委託行爲的設計模式。

  • 類理論(先抽象到父類而後用子類進行特殊化重寫
  • 委託理論
//即不是類也不是對象,包含全部任務均可以使用的具體行爲
Task = {
    setId: function(id) { this.id = id };
    outputId: fuction() { console.log( this.id ); }
};

//讓XYZ委託Task
XYZ = Object.create( Task );

//定義一個對象來存儲數據和行爲
XYZ.prepareTask = function( id, label ){
    this,setId( Id );
    this.label = label;
}
XYZ.outputTaskData = function() {
    this.outputId();
    console.log( this.label );
}


//使用,執行任務XYZ須要兩個兄弟對象(Task和XYZ)協做完成
// ABC = Object.create( Task );
// ABC ... = ...
複製代碼

對象關聯風格代碼的不一樣之處:

  1. 數據成員直接存儲在委託者而不是委託目標;
  2. 兄弟對象通常不會使用相同的命名,提倡使用更有描述性的方法名。尤爲要寫清相應對象行爲的類型。
  3. this會綁定到委託者(隱式綁定)

委託行爲意味着某些對象在找不到屬性或者方法引用時會把這個請求委託給另外一個對象。

在API接口設計中,委託最後在內部實現,不要直接暴露出去。

- 互相委託(禁止)
- 調試(谷歌瀏覽器和其餘瀏覽器的異同)
複製代碼
  • 比較思惟模型

6.2 類和對象

類和對象在實際中的應用場景:建立UI控件(按鈕,下拉列表)。

三種代碼風格:

  1. ES5類
//父類
function Widget(width, height) {
    this.width = width || 50;
    this.height = height || 50;
    this.$elem = null;
}

Widget.prototype.render = function($where){
    if(this.$elem) {
        this.$elem.css({
            width: this.width + 'px',
            height: this.height + 'px'
        }).appendTo( $where );
    }
};

//子類
function Button(width, height, label) {
    //調用‘super’構造函數
    Widget.call( this, width, height ); //顯式僞多態
    this.label = label || "Default";
    
    this.$elem = $("<button>").text( this.label );
}

//讓Button「繼承」Widget
Button.prototype = Object.create( Widget.prototype );

//重寫render(..)
Button.prototype.render = function($where) {
    //"super"調用
    Widget.prototype.render.call( this, $where );//顯式僞多態
    this.#elem.click( this.onClick.bind(this) );
};

Button.prototype.onClick = function(evt) {
    console.log( "Button" + this.label + "clicked");
}

$(document).ready( fucntion()){
    var $body = $(document.body);
    var btn1 = new Button(125, 30, "hello");
    var btn2 = new Button(150, 40, "world");
    
    btn1.render($body);
    btn2.render($body);
}
複製代碼
  1. ES6類
class Widget {
    constructor(width, height) {
        this.width =  width || 50;
        this.height = height || 50;
        this.$elem = null;
    }
    
    render($where){
        if(this.$elem){
            this.$elem.css({
                width: this.width + "px",
                height: this.height + "px"
            }).appendTo( $where );
        }
    }
}

class Button extends Widget {
    constructor(width, height, label) {
        super( width, height );
        this.label = label || "Default";
        this.$elem = $("<button>").text( this.label );
    }
    render($where) {
        super.render( $where );
        this.$elem.click( this.onClick.bind(this) )
    }
    onClick(evt) {
        console.log( "Button" + this.label + "clicked" );
    }
}

$(document).ready( fucntion()){
    var $body = $(document.body);
    var btn1 = new Button(125, 30, "hello");
    var btn2 = new Button(150, 40, "world");
    
    btn1.render($body);
    btn2.render($body);
}
複製代碼
  1. 委託
var Widget = {
    init:function(width, height) {
        this.width = width || 50;
        this.height = height || 50;
        this.$elem = null;
    }
    insert: function($where){
        if(this.$elem){
            this.$elem.css({
                this.width =  width || 50;
                this.height = height || 50;
            }).appendTo( $where );
        }
    }
}

var Button = Object.create( Widget );
Button.setup = function(width, height, label) {
    //委託調用
    this.init( width, height );
    this.label = label || "Default";
    this.$elem = $("<button>").text( this.label );
};
Button.build = function($where) {
    //委託調用
    this.insert( $where );
    this.$elem.click( this.onClick.bind(this) );
};
Button.onClick = function(evt) {
    console.log( "Button" + this.label + "clicked" );
};
$(document).ready( fucntion(){
    var $body = $(document.body);
    
    var btn1 = Object.create(Button);
    btn1.setup( 125, 30, "hello" );
    
    var btn1 = Object.create(Button);
    btn2.setup( 150, 40, "world" );
    
    btn1.build($body);
    btn2.build($body);
} )
複製代碼

對象關聯能夠更好的支持關注分離原則。

6.3 更簡潔的設計

兩個控制器對象 --- 操做網頁中的登陸表單和與服務器進行驗證。

(兩個控制器對象是兄弟關係,不是父子關係。)

6.4 更好的語法

函數名的簡寫,可是須要自我引用時,則使用傳統的具名函數。

6.5 內省

內省就是檢查實例的類型,類實例的內省主要目的是經過建立方式來判斷對象的結構和功能。

function Foo() {/*..*/}
Foo.prototype...

function Bar() {/*..*/}
Bar.prototype = Object.create( Foo.prototype );

var b1 = new Bar( "b1" );

Bar.prototype instanceof Foo; //true
Object.getPrototypeOf( Bar.prototype ) === Foo.prototype; //true
Foo.prototype.isPrototypeOf( Bar.prototype ); //true

b1 instanceof Bar; //true
b1 instanceof Foo; //true
Object.getPrototypeOf(b1) === Bar.prototype; //true
Foo.prototype.isPrototypeOf( b1 ); //true
Bar.prototype.isPrototypeOf( b1 ); //true
複製代碼
var Foo = { /*..*/ };

var Bar = Object.create( Foo );
Bar...

var b1 = Object.create( Bar );

Foo.isPrototypeOf( Bar ); //true
Object.getPrototypeOf( Bar ); //true

Foo.isPrototypeOf(b1); //true
Bar.isPrototypeOf(b1); //true
Object.getPrototypeOf( b1 ) === bar; //true
複製代碼
相關文章
相關標籤/搜索