備注: 本文很多內容都是從byvoid同學的nodejs開發里面摘取出來的,是我當時的讀書筆記,看過很多關於js高級特性介紹的書,我覺得這本書介紹的最同屬易懂當然各有各的愛好,放出來,全當復習
JavaScript 高級特性
1. 作用域
作用域(scope)是結構化編程語言中的重要概念,它決定了變量的可見范圍和生命周期,正確使用作用域可以使代碼更清晰、易懂。作用域可以減少命名沖突,而且是垃圾回收的基本單元。JavaScript 的作用域不是以花括號包圍的塊級作用域(block scope) ,這個特性經常被大多數人忽視,因而導致莫名其妙的錯誤。
if (true) {
var somevar = 'value';
}
console.log(somevar); // 輸出 value
在其他大多數類C語言中會出現變量未定義的錯誤,而在Javascript中卻完全合法,這是因為Javascript中的作用域完全由函數
來決定,if,for語句中的花括號不是獨立的作用域
1.1 函數作用域
不同於大多數類 C 語言,由一對花括號封閉的代碼塊就是一個作用域, JavaScript 的作用域是通過函數來定義的,在一個函數中定義的變量只對這個函數內部可見,我們稱為函數作用域。在函數中引用一個變量時, JavaScript 會先搜索當前函數作用域,或者稱為“局部作用域”,如果沒有找到則搜索其上層作用域,一直到全局作用域。
var v1 = 'v1';
var f1 = function() {
console.log(v1); // 輸出 v1
};
f1();
var f2 = function() {
var v1 = 'local';
console.log(v1); // 輸出 local
};
f2();
以上事例十分明了,變量的搜索順序,由內到外。直到全局作用域
-
看以下代碼:
var scope = 'global';
var f = function() {
console.log(scope); // 輸出 undefined
var scope = 'f';
}
f();這是 JavaScript 的一個特性,按照作用域搜索順序,在 console.log 函數訪問 scope 變量時, JavaScript會先搜索函數 f 的作用域,恰巧在 f 作用域里面搜索到 scope 變量,所以上層作用域中定義的 scope 就被屏蔽了,但執行console.log 語句時, scope 還沒被定義,或者說初始化,所以得到的就是 undefined 值。我們還可以從另一個角度來理解:對於開發者來說,在訪問未定義的變量或定義了但沒有初始化的變量時,獲得的值都是 undefined。於是我們可以認為,無論在函數內什么地方定義的變量,在一進入函數時就被定義了,但直到 var 所在的那一行它才被初始化,所以在這之前引用到的都是 undefined 值。
-
函數作用域的嵌套
var f = function() {
var scope = 'f0';
(function() {
var scope = 'f1';
(function() {
console.log(scope); // 輸出 f1
})();
})();
};
f();
上面是一個函數作用域嵌套的例子,我們在最內層函數引用了 scope 變量,通過作用
域搜索,找到了其父作用域中定義的 scope 變量。有一點需要注意:
函數作用域的嵌套關系是在定義時決定的,而不是調用時決定,也就是說javascript的作用域是靜態作用域,又叫詞法作用域,這是因為作用域的嵌套關系可以在語法分析階段確定,而不必等到運行時確定demo var scope = 'top'; var f1 = function() { console.log(scope); }; f1(); // 輸出 top var f2 = function() { var scope = 'f2'; f1(); }; f2(); // 輸出 top 以上代碼證明,作用域的嵌套關系是在定義時確定,而不是調用時確定
1.2全局作用域
在 JavaScript 中有一種特殊的對象稱為 全局對象。這個對象在Node.js 對應的是global對象,在瀏覽器中對應的是 window 對象。由於全局對象的所有屬性在任何地方都是可見的,所以這個對象又稱為 全局作用域。全局作用域中的變量不論在什么函數中都可以被直接引用,而不必通過全局對象。
滿足以下條件的變量屬於全局作用域:
- 在最外層定義的變量
- 全局對象的屬性
- 在任何地方隱式定義的變量(未聲明直接賦值的變量)
需要格外注意的是第三點,在任何地方隱式定義的變量都會定義在全局作用域當中,即不通過var定義而直接賦值的變量。這一點經常被人遺忘,而模塊兒化編程的一個重要原則就是避免使用全局變量,所以在任何地方都不應該隱式定義變量
2. 閉包
閉包的嚴格定義是“由函數(環境)及其封閉的自由變量組成的集合體。”通俗地講, JavaScript 中每個的函數都是一個閉包,但通常意義上嵌套的函數更能夠體現出閉包的特性,請看下面的例子:
var generateClosure = function() {
var count = 0;
var get = function() {
count ++;
return count;
};
return get;
};
var counter = generateClosure();
console.log(counter()); // 輸出 1
console.log(counter()); // 輸出 2
console.log(counter()); // 輸出 3
按照通常命令式編程思維的理解, count 是generateClosure 函數內部的變量,它的生命周期就是 generateClosure 被調用的時期,當 generateClosure 從調用棧中返回時, count 變量申請的空間也就被釋放。問題是,在generateClosure調用結束counter() 卻引用了“已經釋放了的” count變量,而且非但沒有出錯,反而每次調用 counter() 時還修改並返回了 count 。
這正是所謂閉包的特性。當一個函數返回它內部定義的函數時,就形成了閉包,閉包不但包括被返回的函數,還包括這個函數的定義環境。我們可以理解為,在 generateClosure() 返回 get 函數時,私下將 get 可能引用到的 generateClosure() 函數的內部變量(也就是 count 變量)也返回了,並在內存中生成了一個副本,之后 generateClosure() 返回函數的多個閉包實例 就是相互獨立的了。
在實際操作當中,閉包存在有三個必要條件:
- 作用域的嵌套
- 內部函數對外部函數變量進行了引用
- 內部函數被調用了,不論在何處被調用
2.1閉包的用途
閉包有兩個主要的用途,一個是實現嵌套的回調函數實現異步編程,二是隱藏對象的細節實現模塊兒化編程
- 由於閉包機制的存在,即使外層函數執行完畢,其作用域內申請的變量也不會釋放,因為里層的函數還有可能引用到這些變量,這樣就完美的實現了嵌套的異步回調
3. 對象
JavaScript 中的對象實際上就是一個由屬性組成的關聯數組,屬性由名稱和值組成,值的類型可以是任何數據類型,或者函數和其他對象。注意 JavaScript 具有函數式編程的特性,所以函數也是一種變量,大多數時候不用與一般的數據類型區分
3.1 對象的創建方式
- 屬性追加
var foo = {};
foo.prop_1 = 'bar';
foo.prop_2 = false;
foo.prop_3 = function() {
return 'hello world';
}
console.log(foo.prop_3());
使用關聯數組的方式創建對象
var foo = {};
foo['prop1'] = 'bar';
foo['prop2'] = false;
foo['prop3'] = function() {
return 'hello world';
}
console.log(foo.prop_3());
在 JavaScript 中,使用句點運算符和關聯數組引用是等價的,也就是說任何對象(包括this 指針)都可以使用這兩種模式。使用關聯數組的好處是,在我們不知道對象的屬性名稱的時候,可以用變量來作為關聯數組的索引
2. 對象字面量
var foo = {
'prop1': 'bar',
prop2: 'false',
prop3: function (){
return 'hello world';
}
};
這種定義方法稱為對象的初始化器,注意,使用初始化器時,對象屬性名稱是否加引號是可選的,除非屬性名稱中有空格或者其他可能造成歧義的字符,否則沒有必要使用引號。
3. 構造函數
構造函數的方式,可以允許我們多個規划好的對象
3.2上下文對象
在 JavaScript 中, 上下文對象就是 this 指針,即被調用函數所處的環境。上下文對象的作用是在一個函數內部引用調用它的對象本身, JavaScript 的任何函數都是被某個對象調用的,包括全局對象,所以 this 指針是一個非常重要的東西。this指針不屬於任何函數,而是函數被調用時的所屬對象在。 JavaScript 中,本質上,函數類型的變量是指向這個函數實體的一個引用,在引用之間賦值不會對對象產生復制行為。我們可以通過函數的任何一個引用調用這個函數,不同之處僅僅在於上下文。
var someuser = {
name: 'byvoid',
func: function() {
console.log(this.name);
}
};
var foo = {
name: 'foobar'
};
someuser.func(); // 輸出 byvoid
foo.func = someuser.func;
foo.func(); // 輸出 foobar
name = 'global';
func = someuser.func;
func(); // 輸出 global
仔細觀察上面的例子,使用不同的對象引用來調用同一個函數時,this指針永遠是這個引用所屬的對象,在前面的章節中我們提到了 JavaScript 的函數作用域是靜態的,也就是說一個函數的可見范圍是在預編譯的語法分析中就可以確定的,而上下文對象則可以看作是靜態作用域的補充。
- call和apply
在 JavaScript 中, call 和 apply 是兩個神奇的方法,但同時也是容易令人迷惑的兩個方法,call 和 apply 的功能是以不同的對象作為上下文來調用某個函數。簡而言之,就是允許一個對象去調用另一個對象的成員函數。乍一看似乎很不可思議,而且容易引起混亂,但其實javascript當中並沒有嚴格的所謂成員函數的概念,函數與對象的所屬關系只有在調用的時候才展現出來
call 和 apply 的功能是一致的,兩者細微的差別在於 call 以參數表來接受被調用函
數的參數,而 apply 以數組來接受被調用函數的參數。
- func.call(thisArg, arg1[, arg2[, ...]])
- func.apply(thisArg, argsArray])
var someuser = {
name: 'irick',
display: function(words) {
console.log(this.name + ' says ' + words);
}
};
var foo = {
name: 'foobar'
};
someuser.display.call(foo, 'hello'); // 輸出 foobar says hello
- bind方法
bind 方法來永久地綁定函數的執行上下文,使其無論被誰調用,上下文對象都是固定的。- func.bind(thisArg[, arg1[, arg2[, ...]]])
var someuser = {
name: 'irick',
func: function() {
console.log(this.name);
}
};
var foo = {
name: 'foobar'
};
foo.func = someuser.func;
foo.func(); // 輸出 foobar
foo.func1 = someuser.func.bind(someuser);
foo.func1(); // 輸出 irick
func = someuser.func.bind(foo);
func(); // 輸出 foobar
func2 = func;
func2(); // 輸出 foobar
bind 方法還有一個重要的功能:綁定參數表,如下例所示。
var person = {
name: 'irick',
says: function(act, obj) {
console.log(this.name + ' ' + act + ' ' + obj);
}
};
person.says('loves', 'kciri'); // 輸出 irick loves kciri
byvoidLoves = person.says.bind(person, 'loves');
byvoidLoves('you'); // 輸出 irick loves you
3.3 原型
原型是 JavaScript 面向對象特性中重要的概念,在絕大多數的面向對象語言中,對象是基於類的(例如 Java 和 C++ ),對象是類實例化的結果。而在JavaScript 語言中,沒有類的概念①,對象由對象實例化。打個比方來說,基於類的語言中類就像一個模具,對象由這個模具澆注產生,而基於原型的語言中,原型就好像是一件藝術品的原件,我們通過一台 100% 精確的機器把這個原件復制出很多份。
通過原型構造對象:
function Person() {}
Person.prototype.name = 'BYVoid';
Person.prototype.showName = function () {
console.log(this.name);
};
var person = new Person();
person.showName();
上面這段代碼使用了原型而不是構造函數初始化對象。這樣做與直接在構造函數內定義屬性有什么不同呢?
- 構造函數內定義的屬性繼承方式與原型不同,子對象需要顯式調用父對象才能繼承構
造函數內定義的屬性。 - 構造函數內定義的任何屬性,包括函數在內都會被重復創建,同一個構造函數產生的
兩個對象不共享實例。 - 構造函數內定義的函數有運行時閉包的開銷,因為構造函數內的局部變量對其中定義
的函數來說也是可見的。
下面這端代碼能夠驗證以上問題:
function Foo() {
var innerVar = 'hello';
this.prop1 = 'BYVoid';
this.func1 = function(){
innerVar = '';
};
}
Foo.prototype.prop2 = 'Carbo';
Foo.prototype.func2 = function () {
console.log(this.prop2);
};
var foo1 = new Foo();
var foo2 = new Foo();
console.log(foo1.func1 == foo2.func1); // 輸出 false
console.log(foo1.func2 == foo2.func2); // 輸出 true
盡管如此,並不是說在構造函數內創建屬性不好,而是兩者各有適合的范圍。那么我們什么時候使用原型,什么時候使用構造函數內定義來創建屬性呢?
- 除非必須用構造函數閉包,否則盡量用原型定義成員函數,因為這樣可以減少開銷。
- 盡量在構造函數內定義一般成員,尤其是對象或數組,因為用原型定義的成員是多個實例共享的。
3.4 原型鏈
JavaScript 中有兩個特殊的對象, Object 與 Function,它們都是構造函數,用於生成對象。Object.prototype 是所有對象的祖先, Function.prototype 是所有函數的原型,包括構造函數。我把 JavaScript 中的對象分為三類,一類是用戶創建的對象,一類是構造函數對象,一類是原型對象。用戶創建的對象,即一般意義上用 new 語句顯式構造的對象。構造函數對象指的是普通的構造函數,即通過 new 調用生成普通對象的函數。原型對象特指構造函數 prototype 屬性指向的對象。這三類對象中每一類都有一個__proto__ 屬性,它指向該對象的原型,從任何對象沿着它開始遍歷都可以追溯到 Object.prototype。構造函數對象有prototype 屬性,指向一個原型對象,通過該構造函數創建對象時,被創建對象的 proto 屬性將會指向構造函數的 prototype 屬性。原型對象constructor屬性,指向它對應的構造函數。讓我們通過下面這個例子來理解原型:
function Foo() {}
Object.prototype.name = 'My Object';
Foo.prototype.name = 'Bar';
var obj = new Object();
var foo = new Foo();
console.log(obj.name); // 輸出 My Object
console.log(foo.name); // 輸出 Bar
console.log(foo.__proto__.name); // 輸出 Bar
console.log(foo.__proto__.__proto__.name); // 輸出 My Object
console.log(foo. __proto__.constructor.prototype.name); // 輸出 Bar
在 JavaScript 中,繼承是依靠一套叫做原型鏈(prototype chain)的機制實現的。屬性繼承的本質就是一個對象可以訪問到它的原型鏈上任何一個原型對象的屬性。