學了多種語言,發現
javascript
的this
是最難以捉摸的。this
不就是指向當前對象的指針嗎?可是結合上下文來看,卻又往往不知道this
到底指的是誰了,所以Javascript
最主要的兩個知識點,除了閉包,就是this
了。
1. 關于this
this
關鍵字是javascript
中最復雜的機制之一。它是一個很特別的關鍵字,被自動定義在 所有函數的作用域中。
this
提供了一種更優雅的方式來隱式“傳遞”一個對象引用,因此可以將API設計
得更加簡潔并且易于復用。
function identify() {
return this.name.toUpperCase();
}
var me = {
name: "Kyle"
};
identify.call( me ); // KYLE
this
并不像我們所想的那樣指向函數本身。
function foo(num) {
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
console.log( foo.count ); // 0
函數內部代碼this.count
中的this
并不是指向那個函數對象,所以雖然屬性名相同,根對象卻并不相同,困惑隨之產生。
函數內部代碼this.count
最終值為NaN
,同時也是全局變量。
可以使用函數名稱標識符來代替this
來引用函數對象。這樣,更像是靜態變量。
function foo(num) {
foo.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
console.log( foo.count ); // 4
另外一種方式是強制this
指向foo
函數對象。
function foo(num) {
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo.call(foo, i );
}
}
console.log( foo.count ); // 4
this
到底是什么
this
是在運行時進行綁定的,并不是在編寫時綁定,它的上下文取決于函數調 用時的各種條件。this
的綁定和函數聲明的位置沒有任何關系,只取決于函數的調用方式。
調用位置
函數被調用的位置。每個函數的 this 是在調用 時被綁定的,完全取決于函數的調用位置,因為它決定了this
的綁定。
function baz() {
// 當前調用棧是:baz
// 因此,當前調用位置是全局作用域
console.log( "baz" );
bar(); // <-- bar 的調用位置
}
function bar() {
// 當前調用棧是 baz -> bar
// 因此,當前調用位置在 baz 中
console.log( "bar" );
foo(); // <-- foo 的調用位置
}
function foo() {
// 當前調用棧是 baz -> bar -> foo
// 因此,當前調用位置在 bar 中
console.log( "foo" );
}
baz(); // <-- baz 的調用位置
1.1 綁定規則
默認綁定
聲明在全局作用域中的變量就是全局對象的一個同名屬性。
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
在本 例中,函數調用時應用了this
的默認綁定,因此this
指向全局對象。
foo()
是直接使用不帶任何修飾的函數引用進行調用的,因此只能使用默認綁定,無法應用其他規則。
如果使用嚴格模式,那么全局對象將無法使用默認綁定,因此this
會綁定到 undefined。
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined
隱式綁定
如果調用位置是有上下文對象,或者被某個對象擁有或者包含,那么就可能隱式綁定。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var obj1 = {
a: 42,
obj: obj
};
obj.foo(); // 2
obj1.obj.foo(); // 2
當函數引用有上下文對象時,隱式綁定規則會把函數調用中的this
綁定到這個上下文對象。因為調 用foo()
時this
被綁定到obj
,因此this.a
和obj.a
是一樣的。
對象屬性引用鏈中只有最頂層或者說最后一層會影響調用位置。
隱式綁定的函數可能會丟失綁定對象,而應用默認綁定,把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 doFoo(fn) {
// fn 其實引用的是 foo
fn(); // <-- 調用位置!
}
doFoo( obj.foo ); // "oops, global"
bar
是obj.foo
的一個引用,bar()
其實是一個不帶任何修飾的函數調用。
參數傳遞其實就是一種隱式賦值,因此我們傳入函數時也會被隱式賦值,所以結果一樣。
顯式綁定
可以使用函數的call(..)
和apply(..)
方法實現顯式綁定。
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2
如下例子,無論bar
綁定到哪個對象上,foo
始終綁定在obj
上,稱之為硬綁定。
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
var bar = function() {
foo.call( obj );
};
bar.call( window ); // 2
在 ES5 中提供了內置的方法Function.prototype.bind
就是硬綁定。
如果你把null
或者undefined
作為this
的綁定對象傳入call
、apply
或者 bind
,這些值在調用時會被忽略,實際應用的是默認綁定規則。
new
綁定
JavaScript
中new
的機制實 際上和面向類的語言完全不同。
在JavaScript
中,構造函數只是一些 使用new
操作符時被調用的函數。它們并不會屬于某個類,也不會實例化一個類。實際上,它們甚至都不能說是一種特殊的函數類型,它們只是被 new 操作符調用的普通函數而已。
使用new
來調用函數,或者說發生構造函數調用時,會自動執行下面的操作。
創建(或者說構造)一個全新的對象。
這個新對象會被執行[[原型]]連接。
這個新對象會綁定到函數調用的this。
如果函數沒有返回其他對象,那么
new
表達式中的函數調用會自動返回這個新對象。
function foo(p1,p2) {
this.val = p1 + p2;
}
// 之所以使用 null 是因為在本例中我們并不關心硬綁定的 this 是什么
// 反正使用 new 時 this 會被修改
var bar = foo.bind( null, "p1" );
var baz = new bar( "p2" );
baz.val; // p1p2
綁定規則優先級: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
2. 對象
對象的兩種形式定義:聲明(文字)形式和構造形式。
var myObj = {
key: value
// ...
};
var myObj = new Object();
myObj.key = value;
六種主要類型: string
,number
,boolean
,null
,undefined
,object
除object
外的5種類型為簡單基本類型,本身并不是對象,但是typeof null
會返回字符串 "object"。
內置對象:String
,Number
,Boolean
,Object
,Function
,Array
,Date
,RegExp
,Error
var strPrimitive = "I am a string";
typeof strPrimitive; // "string"
strPrimitive instanceof String; // false
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
對象,可以訪問屬性和方法。
對于Object
、Array
、Function
和RegExp
來說,無論使用文字形式還是構 造形式,它們都是對象,不是字面量。
屬性
屬性名永遠是字符串,雖然在數組下標中使用的的確是數字,但是在對象屬性名中數字會被轉換成字符串。
ES6 增加了可計算屬性名,最常用的場景可能是 ES6 的符號(Symbol)。
var prefix = "foo";
var myObject = {
[prefix + "bar"]:"hello",
[prefix + "baz"]: "world"
};
myObject["foobar"]; // hello
myObject["foobaz"]; // world
如果你試圖向數組添加一個屬性,但是屬性名“看起來”像一個數字,那它會變成 一個數值下標
var myArray = [ "foo", 42, "bar" ];
myArray["3"] = "baz";
myArray.length; // 4
myArray[3]; // "baz"
復制對象
對于JSON
安全的對象來說,有一種巧妙的復制方法:
var newObj = JSON.parse( JSON.stringify( someObj ) );
ES6 定義了Object.assign(..)
方法來實現淺復制。
屬性描述符
三個特性:writable(可寫)、 enumerable(可枚舉)和 configurable(可配置)。
var myObject = {
a:2
};
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
在創建普通屬性時屬性描述符會使用默認值,我們也可以使用 Object.defineProperty(..)
來添加一個新屬性或者修改一個已有屬性(如果它是configurable
)并對特性進行設置。
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true
} );
myObject.a; // 2
writable
決定是否可以修改屬性的值,如果在嚴格模式下,這 種方法會出錯(TypeError)。
把configurable
修改成 false 是單向操作,無法撤銷!不管是不是處于嚴格模式,嘗 試修改一個不可配置的屬性描述符都會出錯(TypeError)。
屬性是不可配置時使用 delete
也會失敗。
如果把enumerable
設置成false
,這個屬性就不會出現在枚舉中(比如for..in循環
),雖然仍 然可以正常訪問它。
不變性
常量: 結合writable:false
和configurable:false
就可以創建一個真正的常量屬性(不可修改、 重定義或者刪除)
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
});
禁止擴展: 如果你想禁止一個對象添加新屬性并且保留已有屬性,可以使用Object.preventExtensions(..)
密封: Object.seal(..)
會創建一個“密封”的對象,這個方法實際上會在一個現有對象上調用Object.preventExtensions(..)
并把所有現有屬性標記為configurable:false
。
凍結: Object.freeze(..)
會創建一個凍結對象,這個方法實際上會在一個現有對象上調用Object.seal(..)
并把所有“數據訪問”屬性標記為writable:false
,這樣就無法修改它們 的值。
get和set
var myObject = {
// 給 a 定義一個 getter
_a:2,
get a() {
return this.a;
},
// 給 a 定義一個 setter
set a(_a){
this._a = _a;
}
};
Object.defineProperty(
myObject, // 目標對象
"b", // 屬性名
{
// 描述符
// 給 b 設置一個 getter
get: function(){
return this.a * 2
},
// 確保 b 會出現在對象的屬性列表中
enumerable: true
}
);
myObject.a; // 2
myObject.b; // 4
在不訪問屬性值的情況下判斷對象中是否存在這個屬性:
var myObject = {
a:2
};
("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "b" ); // false
in
操作符會檢查屬性是否在對象及其 [[Prototype]] 原型鏈中,相比之下,hasOwnProperty(..)
只會檢查屬性是否在 myObject 對象中,不會檢查 [[Prototype]] 鏈。
有的對象可能沒有連接到Object.prototype
,可以使用Object.prototype.hasOwnProperty. call(myObject,"a")
進行判斷。
propertyIsEnumerable(..)
會檢查給定的屬性名是否直接存在于對象中(而不是在原型鏈 上)并且滿足enumerable:true
。
Object.keys(..)
會返回一個數組,包含所有可枚舉屬性,Object.getOwnPropertyNames(..)
會返回一個數組,包含所有屬性,無論它們是否可枚舉。
數組有內置的@@iterator
,因此for..of
可以直接應用在數組上。
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 }
手動定義@@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)
};
} };
} } );
3. 原型
JavaScript
中的對象有一個特殊的 [[Prototype]] 內置屬性,其實就是對于其他對象的引用。幾乎所有的對象在創建時 [[Prototype]] 屬性都會被賦予一個非空的值。
對于默認的 [[Get]] 操作來說,第一步是檢查對象本身是 否有這個屬性,如果有的話就使用它。但是如果不存在與對象本身,就需要會繼續訪問對象的 [[Prototype]] 鏈。
var anotherObject = {
a:2
};
// 創建一個關聯到 anotherObject 的對象
var myObject = Object.create( anotherObject );
myObject.a; // 2
任何可以通過原型鏈訪問到并且是enumerable
的屬性都會被枚舉。
使用in
操作符來檢查屬性在對象中是否存在時,同樣會查找對象的整條原型鏈(無論屬性是否可枚舉)。
所有普通的 [[Prototype]] 鏈最終都會指向內置的Object.prototype
,它包含 JavaScript
中許多通用的功能,比如.toString()
。
原型鏈上層時myObject.foo = "bar"
會出現的三種情況:
如果[[Prototype]]鏈上層存在名為foo的普通數據訪問屬性并且不是只讀,就會直接在 myObject 中添加一個名為 foo 的新 屬性,它是屏蔽屬性。
如果[[Prototype]]鏈上層存在名為foo的普通數據訪問屬性并且只讀,則無法修改已有屬性或者在 myObject 上創建屏蔽屬性。
如果在[[Prototype]]鏈上層存在
foo
并且它是一個setter
,那就一定會 調用這個setter
。
有些情況下會隱式產生屏蔽:
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
++
操作首先會通過 [[Prototype]] 查找屬性a
并從anotherObject.a
獲取當前屬性值2
,然后給這個值加1
,接著用 [[Put]] 將值3
賦給myObject
中新建的屏蔽屬性a
。
類
所有的函數默認都會擁有一個 名為prototype
的公有并且不可枚舉的屬性,它會指向另一個對象,這個對象通常被稱為該對象的原型。
function Foo() {
// ...
}
Foo.prototype; // { }
在方法射調用new
時創建對象時,該對象最后會被關聯到這個方法的prototype
對象上。
function Foo() {
// ...
}
var a = new Foo();
Object.getPrototypeOf( a ) === Foo.prototype; // true
new Foo()
會生成一個新對象,這個新對象的內部鏈接[[Prototype]]關聯的是 Foo.prototype
對象。最后我們得到了兩個對象,它們之間互相關聯。
在JavaScript
中,我們并不會將一個對象(“類”)復制到另一個對象(“實例”),只是將它們關聯起來。這個機制通常被稱為原型繼承。
構造函數
使用new
創建的對象會調用類的構造函數。
function Foo() {
// ...
}
Foo.prototype.constructor === Foo; // true
var a = new Foo();
a.constructor === Foo; // true
Foo.prototype
默認有一個公有并且不可枚舉的屬性.constructor
,這個屬性引用的是對象關聯的函數。
可以看到通過“構造函數”調用new Foo()
創建的對象也有一個.constructor
屬性,指向 “創建這個對象的函數”。
函數本身并不是構造函數,然而,當你在普通的函數調用前面加上new
關鍵字之后,就會把這個函數調用變成一個“構造函數 調用”。實際上,new
會劫持所有普通函數并用構造對象的形式來調用它。
在JavaScript
中對于“構造函數”最準確的解釋是,所有帶new
的函數調用。
如果 你創建了一個新對象并替換了函數默認的.prototype
對象引用,那么新對象并不會自動獲 得.constructor
屬性。
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 創建一個新原型對象
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!
可以給 Foo.prototype 添加一個 .constructor 屬性,不過這需要手動添加一個符
合正常行為的不可枚舉屬性。
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 創建一個新原型對象
Object.defineProperty( Foo.prototype, "constructor" , {
enumerable: false,
writable: true,
configurable: true,
value: Foo // 讓 .constructor 指向 Foo
});
繼承
典型的“原型風格”:
function Foo(name) {
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.constructor 了
// 如果你需要這個屬性的話可能需要手動修復一下它
Bar.prototype = Object.create( Foo.prototype );
Bar.prototype.myLabel = function() {
return this.label;
};
var a = new Bar( "a", "obj a" );
a.myName(); // "a"
a.myLabel(); // "obj a"
ES6 開始可以直接修改現有的Bar.prototype
Object.setPrototypeOf( Bar.prototype, Foo.prototype );
檢查一個實例的繼承關系
// 非常簡單:b 是否出現在 c 的 [[Prototype]] 鏈中
b.isPrototypeOf( c );
Object.getPrototypeOf( a ) === Foo.prototype; // true
// 非標準的方法訪問內部 [[Prototype]] 屬性
a.__proto__ === Foo.prototype; // true
寫了這么多,實在寫不下去了。《你不知道的js》都是滿滿的干貨,筆記記到這里發現好多知識都非常有用,沒辦法省略。幾下這些筆記,也是為了復習一下,以免忘得太快了,所以受益的終究還是自己呀。