你不知道的 JavaScript 上卷 第二部分 筆記

第1章 關於 this

1.1 爲何要用 this

this提供了一種更優雅的方式「傳遞」一個對象引用,所以能夠將API設計得更加簡潔而且易於複用。javascript

var obj = {
    name: 'Reader',
    speak: function() {
        console.log(this.name);
    }
};

obj.speak(); // Reader
複製代碼

1.2 誤解

有兩種對this常見的誤解:前端

  1. 指向自身
  2. this的做用域
// 第一個誤解: this 指向自身
function foo(){
    console.log(this.count);
}

foo.count = 4;
var count = 3;
foo();
複製代碼

this並不指向foo函數,而是查找外層做用域,最終找到全局做用域的countjava

// 第二個誤解:this 指向函數的做用域
function foo() {
    var a = 2;
    this.bar();
}

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

foo(); // undefined
複製代碼

首先,foo函數向外層做用域找到bar函數,而後逐層向外找a,到全局做用域找到window對象,而後window上沒有a屬性,因此是undfined算法

注意:this在任何狀況都不指向函數的詞法做用域。在 JavaScript 內部,做用域和對象很類似,可是做用域沒法經過 JavaScript 代碼訪問,它存在於 JavaScript 引擎內部。數組

1.3 this 究竟是什麼

this是在運行時綁定的,不是在編寫時綁定,它取決於函數的調用方式。
當一個函數被調用時,會建立一個活動記錄(也稱執行上下文)。這個記錄包含函數在哪裏被調用(調用棧)、函數的調用方式、傳入的參數信息。this就是這個記錄的一個屬性,會在函數執行過程當中用到。promise

第2章 this 全面解析

2.1 調用位置

分析調用位置最重要的是分析調用棧。瀏覽器

動圖來自 前端開發都應該懂的事件循環(event loop)以及異步執行順序(setTimeout、promise和async/await)

2.2 綁定規則

2.2.1 默認綁定

當沒有其餘綁定時,使用默認綁定,非嚴格模式下,this綁定到全局做用域下的全局對象window;嚴格模式下,不能使用全局對象,所以this會綁定到undefined安全

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

var a = 1;

foo(); // 1
複製代碼

2.2.2 隱式綁定

查看調用位置是否有上下文對象,或者說是否被某個擁有或者包含,若是是,this「至關於」那個對象的引用。app

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

var a = 1;

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

obj.foo(); // 2
foo(); // 1
複製代碼
var foo1 = obj.foo;
foo1(); // 1
複製代碼
1





undefined
複製代碼

此時將objfoo方法賦給foo1, 此時調用foo1至關於直接調用foo異步

2.2.3 顯式綁定

若是咱們想使用一個對象上的方法,並在某個對象上使用,這個時候就須要顯式綁定,用到call方法和apply方法。
具體用法:Function.prototype.applyFunction.prototype.call

1. 硬綁定

硬綁定是一種很是經常使用的模式,因此ES5提供了內置的方法Function.prototype.bind

// TODO: 理解更爲複雜的bind的實現
// Function.prototype.bind 的 Polyfill(來自MDN)
// Does not work with `new funcA.bind(thisArg, args)`
if (!Function.prototype.bind) (function(){
  var slice = Array.prototype.slice;
  Function.prototype.bind = function() {
    var thatFunc = this, thatArg = arguments[0];
    var args = slice.call(arguments, 1);
    if (typeof thatFunc !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - ' +
             'what is trying to be bound is not callable');
    }
    return function(){
      var funcArgs = args.concat(slice.call(arguments))
      return thatFunc.apply(thatArg, funcArgs);
    };
  };
})();
複製代碼
2. API 調用「上下文」

第三方庫的許多函數,以及JavaScript語言和宿主環境中許多新的內置函數,都提供了一個可選參數,一般被稱爲「上下文」,其做用和bind同樣。

function foo(el) {
    console.log(el, this.id);
}

var obj = {
    id: 'awesome'
};

[1, 2, 3].forEach(foo, obj);
// 1 'awesome'
// 2 'awesome'
// 3 'awesome'
複製代碼

2.2.4 new 綁定

使用new來調用函數,或者說發生構造函數調用時,會自動執行下面的操做。

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

2.3 優先級

new > 顯式綁定 > 隱式綁定 > 默認綁定

顯然,隱式綁定優先級大於默認綁定,由於若是默認綁定優先級大於隱式綁定,則經過對象調用方法時會綁定全局對象而不是綁定該對象。

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

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

var obj1 = {
    a: 3
}

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

上面的例子說明,顯式調用優先級大於隱式調用。最後咱們須要斷定new與顯式綁定和隱式綁定的優先級。

function foo(something){
    this.a = something;
}

var obj1 = {
    foo: foo
};
obj1.foo(2);

var obj2 = new obj1.foo(4);

console.log(obj1.a); // 2
console.log(obj2.a); // 4
複製代碼

能夠看到new綁定比隱式綁定優先級要高。

注意:newcall/apply沒法一塊兒使用,所以沒法直接測試,可是能夠經過硬綁定測試。

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(e)並無修改obj1,而是綁定到一個新的對象。

2.4 綁定例外

2.4.1 被忽略的this

若是把null或者undefined做爲綁定對象傳給call/apply或者bind,這些值在調用是會被忽略,實際應用的是默認綁定規則。
一種常見作法是使用apply來「展開」一個數組,看成參數傳入一個函數。相似的,bind能夠對參數柯里化,這種方法有時很是有用。

function foo(a, b) {
    console.log('a:', a, 'b:', b);
}

foo.apply(null, [2, 3]); // a: 2 b: 3
複製代碼
var bar = foo.bind(null, 2);
bar(3); // a: 2 b: 3
複製代碼

老是使用null做爲綁定對象,會有一些潛在的反作用,若是某個函數使用了this,那麼默認綁定規則會把this綁定到全局對象,這會致使不可預測的結果。

更安全的this

更安全的辦法是傳入一個特殊的對象,把this綁定到這個對象不會有任何反作用。
咱們能夠在忽略this綁定時傳入一個空對象,不會對全局對象產生影響。在 JavaScript 中建立一個空對象最簡單的方法是Object.create(null)Object.create(null)null很像,可是它並不會建立Object.prototype這個委託,因此它更「空」。

function foo(a, b) {
    console.log('a:', a, 'b:', b);
}

var empty = Object.create(null);

foo.apply(empty, [2, 3]); // a: 2 b: 3
複製代碼

2.4.2 間接引用

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...)返回的是foo函數,因此會使用全局對象的a

2.4.3 軟綁定

// TODO:軟綁定的實現
複製代碼

2.5 this詞法

箭頭函數不使用this的四種標準,而是根據外層做用域來肯定this

function foo() {
    return (a) => {
        conosle.log(this.a)
    }
};

var obj1 = {
    a: 2
};

var obj2 = {
    a: 3
}

var bar = foo.call(obj1);
bar.call(obj2); // 2 而不是3
複製代碼

foo.call(obj1);運行完畢時,箭頭函數的this已經綁定在obj1上,已經沒法經過硬綁定從新綁定。

第3章 對象

3.1 語法

var myObject = {
    key: 'value'
}
// 或
var myObj = new Object();
myObject.key = 'value';
複製代碼

3.2 類型

在JavaScript中有六種主要類型:

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

(ES6中,新加了symbol

簡單基本類型(string、boolean、number、null 和 undefined)自己並非對象。null有時會被當成一種對象類型,可是這是語言自己的一個bug,實際上,null自己是基本類型。

內置對象

JavaScript中還有一些對象子類型,一般被稱爲內置對象。有些內置對象的名字看起來和簡單基礎類型同樣,不過實際上它們的關係更復雜。

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

在JavaScript中,他們其實是一些內置函數,這些函數能夠看成構造函數來使用,從而能夠構造一個對應子類型的新對象。

var strPrimitive = "I'm a string";
typeof strPrimitive; // string
strPrimitive instanceof String; // false

var strObject = new String("I'm a string");
typeof strObject; // 'object'
strObject instanceof String; // true
複製代碼

**注意:原始值I'm a string不是一個對象,他只是一個字面量,而且不是一個不可變的值。若是要在這個字面量上執行一些操做,好比獲取長度、訪問其中某個字符等,那須要轉換爲String對象,幸虧在必要時語言會自動把字符串字面量轉換成一個String對象。**number存在相似行爲。

nullundefined沒有對應的構造式,只有文字形式。相反Date只有構造,沒有文字形式。

對於ObjectArrayFunctionRegExp來講,不管使用文字形式仍是構造形式,它們都是對象。
Error對象不多在代碼中顯式建立,通常是在拋出異常時被自動建立。

3.3 內容

鍵訪問obj['key'],屬性訪問obj.key

3.3.1 可計算屬性名

經過表達式來計算屬性名,可使用obj[perfix+name]使用。

3.3.2 屬性與方法

3.3.3 數組

數組也是對象,雖然每一個下標都是整數,仍然能夠給數組添加屬性。

3.3.4 複製對象

對象拷貝分爲深拷貝和淺拷貝,淺拷貝其實只是對原有對象的引用,原對象發生改變則淺拷貝的對象也會發生變化。

var obj = {
    a: 1,
    b: 2,
    c: 3
}

var obj1 = obj;
obj1.a; // 1
obj.a = 2;
obj1.a; // 2
複製代碼

深拷貝比淺拷貝麻煩得多,JavaScript有一種辦法實現深拷貝。

var obj = {a:3};
var newObj = JSON.parse(JSON.stringify(obj));
newObj.a; // 3
obj.a = 2;
newObj.a; // 3
複製代碼

可是這種方法的前提是保證對象是JSON安全的,因此只適用部分狀況。

儘管,JavaScript的Object上有assign方法,他能夠進行對象間的複製,可是仍然不知足深拷貝的要求。
Object.assign的詳細信息:Object.assign() - JavaScript | MDN

var obj = {
    a: 1,
    b: {
        c: 3
    }
}

var newObj = Object.assign({}, obj);
obj.a = 2
newObj.a // 1
obj.b.c = 4;
newObj.b.c; // 4
複製代碼

儘管,複製出了一個newObj,可是它內部的b屬性仍是引用的obj內部的b屬性,仍是淺拷貝。

3.3.5 屬性描述符

Object.getOwnPropertyDescriptor(obj, prop)獲取屬性描述符。
Object.defineProperty(obj, prop, descriptor)添加一個新屬性或者修改一個已有屬性。

3.3.6 不變性

1.對象常量
結合writeable: falseconfigurable:false就能夠建立一個真正的常量屬性(不可被修改、重定義或者刪除)
2.禁止擴展
Object.preventExtensions(obj)禁止一個對象添加新屬性而且保留已有屬性。
3.密封
Object.seal在禁止擴展基礎上,把現有屬性標記爲configurable:false
4.凍結
Object.freeze在密封的基礎上,把全部屬性標記爲writable:false

**注意:**這些功能只能做用在一個對象的鍵上,可是若是某一個鍵的值是一個對象,該對象不會受到影響,即嵌套對象內部的對象的可變性不受影響。若是像深度修改,逐級遍歷內部的對象。

3.3.7 [[Get]]

var myObject = {
    a: 2
};

myObject.a;
複製代碼

在語言規範中,myObject.amyOjbect上實際上實現了[[Get]]操做,首先在對象中查找是否有名稱相同的屬性,若是找到就會返回這個屬性,不然就會在原型鏈上尋找。
TODO:原型鏈見後文

3.3.8 [[Put]]

有獲取屬性的操做,天然有對應的[[Put]]操做,[[Put]]算法大體會檢查下面這些內容:

  1. 屬性是不是訪問描述符(參見3.3.9節)?若是是而且存在setter就調用setter
  2. 屬性的數據描述符中writable是不是false?若是是,在非嚴格模式下靜默失敗,在嚴格模式下拋出TypeError異常
  3. 若是不是,將該值設置爲屬性的值

若是對象中不存在這個屬性,[[Put]]操做會更加複雜,TODO:在後文討論。

3.3.9 Getter和Setter

在ES5中可使用gettersetter部分改寫默認操做,可是隻能應用在單個屬性上,沒法應用在整個對象上。getter是一個隱藏函數,會在獲取屬性值時調用。setter也是一個隱藏函數,會在設置屬性值時調用。

var myObject = {
    get a() {
        console.log('this is getter');
        return 2;
    }
}
myObject.a; 
// this is getter
// 2
複製代碼
Object.defineProperty(
    myObject,
    "b",
    {
        get: function(){ return this.a * 2 },
        enumerable: true,
        configurable: true
    }
);
myObject.b;
// this is getter
// 4
複製代碼

爲了讓屬性更合理,還應當定義setter,和你指望的同樣,setter會覆蓋單個屬性默認的[[Put]]操做。一般來講gettersetter是成對出現的(只定義一個的話一般會產生意料以外的行爲):

var myObject = {
    get a() {
        return this._a_;
    },
    set a(val) {
        this._a_ = val * 2;
    }
}

myObject.a = 2;
myObject.a; // 4
複製代碼

3.3.10 存在性

訪問一個對象個屬性返回爲undefined,可是這個屬性是不存在,仍是存在可是值是undefined,如何區分這兩種狀況?

咱們能夠在不訪問屬性值狀況下判斷對象中是否存在這個屬性:

var myObject = {
    a: 2
};

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

myObject.hasOwnProperty('a'); // true
myObject.hasOwnProperty('b'); // false
複製代碼

in操做符會檢查屬性是否咋愛對象及其[[Prototype]]原型鏈中。TODO:參見第5章。相比之下,hasOwnProperty只會檢查屬性是否在myObject中,不會檢查原型鏈。

Object.keys()返回對象直接包含的全部可枚舉屬性,Object.getOwnPropertyNames()返回對象直接包含的全部屬性,不管是否可枚舉。

3.4 遍歷

for...in循環能夠用來遍歷對象的可枚舉屬性列表(包括原型鏈)。
for(var i = 0; i< len; i++){...}的方式其實不是在遍歷值,而是用下標來指向值而後訪問。
forEach會遍歷數組全部值,並返回回調函數的返回值。
every一直運行回調函數返回fasle
some一直運行到回調函數返回真值

這部分省略for..of的內容,這部分直接看ES6的教程更好些,後面會省略部分ES6的內容。

第4章 混合對象「類」

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

舉個例子,以汽車爲例:

就是圖紙,圖紙上包含了它的各類零件以及它具有什麼樣的功能。

實例化就是按照圖紙造一輛車出來。

// 僞代碼
calss Vehicle{
    engines = 1
    
    run() {
        console.log('run')
    }
    
    toot() {
        console.log('toot')
    }
}
複製代碼

類的實例化是由一個特殊的方法來構造的,這個方法被稱爲構造函數,經過new來調用。別的語言中,這個方法名和類名相同。JavaScript比較特殊,後面會說明。

類一個很重要的特性就是繼承,假設有一個父類,一個子類,子類繼承父類,父類的特性會複製給子類。
用車輛舉例,交通工具是父類,小轎車是子類,它繼承了交通工具的全部特性。
類另外一個重要的特性是多態,用交通工具舉例,交通工具和小轎車均可以駕駛,可是小轎車是四輪驅動的行駛,小轎車的類定義了本身行駛方法,行駛時會使用自身的行駛方法,而不是父類的。

calss Car inherits Vehicle{
    run() {
        console.log('car run')
    }
}
複製代碼

多重繼承就是繼承多個父類,可是JavaScript自己不提供多重繼承。

4.4 混入

在JavaScript中,只存在對象,不存在類,在其餘語言中類表現出來的都是複製行爲,所以JavaScript開發者也想出一個方法來模擬類的複製行爲,這個方法就是混入。

第5章 原型

5.1 [[Prototype]]

JavaScript 中又一個特殊的[[Prototype]]內置屬性,其實就是對其餘對象的飲用。幾乎全部的對象在建立時[[Prototype]]的屬性都會被賦予一個非空的值。
然而雖說[[Prototype]]是一個隱藏屬性,但不少瀏覽器都給每個對象提供__proto__這一屬性,這個屬性就是上文反覆提到的該對象的[[prototype]]。因爲這個屬性不標準,所以通常不提倡使用。

在第3章中說過,當試圖引用對象的屬性時會觸發[[Get]]操做,好比myObject.a,對於默認的[[Get]]操做來講,第一步是檢查對象自己是否有這個屬性,若是有的話就使用它。
可是若是不存在,就須要使用對象的[[Prototype]]鏈了。for...in同理。

var obj = {
    a: '1'
};

var source = {
    b: 'source: 2'
};

obj.__proto__ = source; // 不推薦這麼使用
obj.b; // source: 2
source.b = 'source: 22';
obj.b; // source: 22
複製代碼
'source: 22'
複製代碼
var source = {
    b: 'source: 2'
};

var obj = Object.create(source);
obj.b; // source: 2
複製代碼

5.1.1 Object.prototype

哪裏是[[Prototype]]鏈的「盡頭」?
全部普通的[[Prototype]]最終都會指向內置的Object.prototype

5.1.2 屬性的設置和屏蔽

屬性訪問時,若是不直接存在對象上時,會遍歷原型鏈。若是,在原型鏈上遍歷時,先發現的對象上有該屬性,就取出該屬性,再也不查找後面的對象,就屏蔽了原型鏈後面的同名屬性。

可是在設置屬性時,會有出人意料的行爲發生:

  1. 若是在原型鏈上存在該屬性而且沒有被標記爲只讀,那就直接在對象上添加該屬性。
  2. 若是在原型鏈上存在該屬性但被標記爲只讀,那麼沒法修改已有屬性和在對象上添加新的屬性。嚴格模式下,會拋出一個錯誤;不然這條語句會被忽略。
  3. 若是在原型鏈上存在該屬性而且它是一個setter,那就會調用這個setter,不會被添加到該對象,也不會在對象上從新定義一個setter

5.2 「類」

在JavaScript中,只有對象。

總結到 JavaScript 類這一起的時候,書裏有不少理論性的內容,這部分須要單獨總結,並且工做量還不小,因此等後面有時間了,會嘗試總結一下。

相關文章
相關標籤/搜索