this
提供了一種更優雅的方式「傳遞」一個對象引用,所以能夠將API設計得更加簡潔而且易於複用。javascript
var obj = {
name: 'Reader',
speak: function() {
console.log(this.name);
}
};
obj.speak(); // Reader
複製代碼
有兩種對this
常見的誤解:前端
this
的做用域// 第一個誤解: this 指向自身
function foo(){
console.log(this.count);
}
foo.count = 4;
var count = 3;
foo();
複製代碼
this
並不指向foo
函數,而是查找外層做用域,最終找到全局做用域的count
。java
// 第二個誤解: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 引擎內部。數組
this
是在運行時綁定的,不是在編寫時綁定,它取決於函數的調用方式。
當一個函數被調用時,會建立一個活動記錄(也稱執行上下文)。這個記錄包含函數在哪裏被調用(調用棧)、函數的調用方式、傳入的參數信息。this
就是這個記錄的一個屬性,會在函數執行過程當中用到。promise
分析調用位置最重要的是分析調用棧。瀏覽器
動圖來自 前端開發都應該懂的事件循環(event loop)以及異步執行順序(setTimeout、promise和async/await)當沒有其餘綁定時,使用默認綁定,非嚴格模式下,this
綁定到全局做用域下的全局對象window
;嚴格模式下,不能使用全局對象,所以this
會綁定到undefined
。安全
function foo() {
console.log(this.a);
}
var a = 1;
foo(); // 1
複製代碼
查看調用位置是否有上下文對象,或者說是否被某個擁有或者包含,若是是,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
複製代碼
此時將obj
的foo
方法賦給foo1
, 此時調用foo1
至關於直接調用foo
。異步
若是咱們想使用一個對象上的方法,並在某個對象上使用,這個時候就須要顯式綁定,用到call
方法和apply
方法。
具體用法:Function.prototype.apply、Function.prototype.call
硬綁定是一種很是經常使用的模式,因此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);
};
};
})();
複製代碼
第三方庫的許多函數,以及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'
複製代碼
使用new
來調用函數,或者說發生構造函數調用時,會自動執行下面的操做。
this
new
表達式中的函數調用會自動返回這個對象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
綁定比隱式綁定優先級要高。
注意:new
和call/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
,而是綁定到一個新的對象。
若是把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
綁定時傳入一個空對象,不會對全局對象產生影響。在 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
複製代碼
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
。
// TODO:軟綁定的實現
複製代碼
箭頭函數不使用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
上,已經沒法經過硬綁定從新綁定。
var myObject = {
key: 'value'
}
// 或
var myObj = new Object();
myObject.key = 'value';
複製代碼
在JavaScript中有六種主要類型:
(ES6中,新加了symbol
)
簡單基本類型(string、boolean、number、null 和 undefined)自己並非對象。null有時會被當成一種對象類型,可是這是語言自己的一個bug,實際上,null自己是基本類型。
JavaScript中還有一些對象子類型,一般被稱爲內置對象。有些內置對象的名字看起來和簡單基礎類型同樣,不過實際上它們的關係更復雜。
在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存在相似行爲。
null
和undefined
沒有對應的構造式,只有文字形式。相反Date
只有構造,沒有文字形式。
對於Object
、Array
、Function
和RegExp
來講,不管使用文字形式仍是構造形式,它們都是對象。
Error
對象不多在代碼中顯式建立,通常是在拋出異常時被自動建立。
鍵訪問obj['key']
,屬性訪問obj.key
。
經過表達式來計算屬性名,可使用obj[perfix+name]
使用。
數組也是對象,雖然每一個下標都是整數,仍然能夠給數組添加屬性。
對象拷貝分爲深拷貝和淺拷貝,淺拷貝其實只是對原有對象的引用,原對象發生改變則淺拷貝的對象也會發生變化。
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
屬性,仍是淺拷貝。
Object.getOwnPropertyDescriptor(obj, prop)獲取屬性描述符。
Object.defineProperty(obj, prop, descriptor)添加一個新屬性或者修改一個已有屬性。
1.對象常量
結合writeable: false
和configurable:false
就能夠建立一個真正的常量屬性(不可被修改、重定義或者刪除)
2.禁止擴展
Object.preventExtensions(obj)禁止一個對象添加新屬性而且保留已有屬性。
3.密封
Object.seal在禁止擴展基礎上,把現有屬性標記爲configurable:false
4.凍結
Object.freeze在密封的基礎上,把全部屬性標記爲writable:false
**注意:**這些功能只能做用在一個對象的鍵上,可是若是某一個鍵的值是一個對象,該對象不會受到影響,即嵌套對象內部的對象的可變性不受影響。若是像深度修改,逐級遍歷內部的對象。
var myObject = {
a: 2
};
myObject.a;
複製代碼
在語言規範中,myObject.a
在myOjbect
上實際上實現了[[Get]]
操做,首先在對象中查找是否有名稱相同的屬性,若是找到就會返回這個屬性,不然就會在原型鏈上尋找。
TODO:原型鏈見後文
有獲取屬性的操做,天然有對應的[[Put]]
操做,[[Put]]
算法大體會檢查下面這些內容:
setter
就調用setter
writable
是不是false
?若是是,在非嚴格模式下靜默失敗,在嚴格模式下拋出TypeError
異常若是對象中不存在這個屬性,[[Put]]
操做會更加複雜,TODO:在後文討論。
在ES5中可使用getter
和setter
部分改寫默認操做,可是隻能應用在單個屬性上,沒法應用在整個對象上。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]]
操做。一般來講getter
和setter
是成對出現的(只定義一個的話一般會產生意料以外的行爲):
var myObject = {
get a() {
return this._a_;
},
set a(val) {
this._a_ = val * 2;
}
}
myObject.a = 2;
myObject.a; // 4
複製代碼
訪問一個對象個屬性返回爲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()
返回對象直接包含的全部屬性,不管是否可枚舉。
for...in
循環能夠用來遍歷對象的可枚舉屬性列表(包括原型鏈)。
for(var i = 0; i< len; i++){...}
的方式其實不是在遍歷值,而是用下標來指向值而後訪問。
forEach
會遍歷數組全部值,並返回回調函數的返回值。
every
一直運行回調函數返回fasle
some
一直運行到回調函數返回真值
這部分省略for..of
的內容,這部分直接看ES6的教程更好些,後面會省略部分ES6的內容。
類/繼承描述了一種代碼組織結構形式——一種對真實世界中問題領域的建模方法。
舉個例子,以汽車爲例:
類 就是圖紙,圖紙上包含了它的各類零件以及它具有什麼樣的功能。
實例化就是按照圖紙造一輛車出來。
// 僞代碼
calss Vehicle{
engines = 1
run() {
console.log('run')
}
toot() {
console.log('toot')
}
}
複製代碼
類的實例化是由一個特殊的方法來構造的,這個方法被稱爲構造函數,經過new
來調用。別的語言中,這個方法名和類名相同。JavaScript比較特殊,後面會說明。
類一個很重要的特性就是繼承,假設有一個父類,一個子類,子類繼承父類,父類的特性會複製給子類。
用車輛舉例,交通工具是父類,小轎車是子類,它繼承了交通工具的全部特性。
類另外一個重要的特性是多態,用交通工具舉例,交通工具和小轎車均可以駕駛,可是小轎車是四輪驅動的行駛,小轎車的類定義了本身行駛方法,行駛時會使用自身的行駛方法,而不是父類的。
calss Car inherits Vehicle{
run() {
console.log('car run')
}
}
複製代碼
多重繼承就是繼承多個父類,可是JavaScript自己不提供多重繼承。
在JavaScript中,只存在對象,不存在類,在其餘語言中類表現出來的都是複製行爲,所以JavaScript開發者也想出一個方法來模擬類的複製行爲,這個方法就是混入。
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
複製代碼
哪裏是[[Prototype]]
鏈的「盡頭」?
全部普通的[[Prototype]]
最終都會指向內置的Object.prototype
。
屬性訪問時,若是不直接存在對象上時,會遍歷原型鏈。若是,在原型鏈上遍歷時,先發現的對象上有該屬性,就取出該屬性,再也不查找後面的對象,就屏蔽了原型鏈後面的同名屬性。
可是在設置屬性時,會有出人意料的行爲發生:
setter
,那就會調用這個setter
,不會被添加到該對象,也不會在對象上從新定義一個setter
。在JavaScript中,只有對象。