變量作用域
變量提升
Javascript的函數定義有個特點,它會先掃描整個函數體的語句,把所有申明的變量“提升”到函數頂部:
'use strict';function foo() { var x = 'Hello, ' + y; alert(x); var y = 'Bob';}foo();雖然是strict模式,但語句var x = ‘Hello, ’ + y;并不報錯,原因是變量y在稍后申明了。但是alert顯示Hello, undefined,說明變量y的值為undefined。這正是因為JavaScript引擎自動提升了變量y的聲明,但不會提升變量y的賦值。
全局作用域
不在任何函數內定義的變量就具有全局作用域。實際上,JavaScript默認有一個全局對象window,全局作用域的變量實際上被綁定到window的一個屬性:
'use strict';var course = 'Learn JavaScript';alert(course); // 'Learn JavaScript'alert(window.course); // 'Learn JavaScript'因此,直接訪問全局變量course和訪問window.course是完全一樣的。
由于函數定義有兩種方式,以變量方式var foo = function () {}定義的函數實際上也是一個全局變量,因此,頂層函數的定義也被視為一個全局變量,并綁定到window對象
我們每次直接調用的alert()函數其實也是window的一個變量
JavaScript實際上只有一個全局作用域。任何變量(函數也視為變量),如果沒有在當前函數作用域中找到,就會繼續往上查找,最后如果在全局作用域中也沒有找到,則報ReferenceError錯誤。
名字空間
全局變量會綁定到window上,不同的JavaScript文件如果使用了相同的全局變量,或者定義了相同名字的頂層函數,都會造成命名沖突,并且很難被發現。
減少沖突的一個方法是把自己的所有變量和函數全部綁定到一個全局變量中。例如:
// 唯一的全局變量MYAPP:var MYAPP = {};// 其他變量:MYAPP.name = 'myapp';MYAPP.version = 1.0;// 其他函數:MYAPP.foo = function () { return 'foo';};把自己的代碼全部放入唯一的名字空間MYAPP中,會大大減少全局變量沖突的可能。
許多著名的JavaScript庫都是這么干的:jQuery,YUI,underscore等等。
局部作用域
由于JavaScript的變量作用域實際上是函數內部,我們在for循環等語句塊中是無法定義具有局部作用域的變量的:
'use strict';function foo() { for (var i=0; i<100; i++) { // } i += 100; // 仍然可以引用變量i}為了解決塊級作用域,ES6引入了新的關鍵字let,用let替代var可以申明一個塊級作用域的變量:
'use strict';function foo() { var sum = 0; for (let i=0; i<100; i++) { sum += i; } i += 1; // SyntaxError}常量
由于var和let申明的是變量,如果要申明一個常量,在ES6之前是不行的,我們通常用全部大寫的變量來表示“這是一個常量,不要修改它的值”:
var PI = 3.14;ES6標準引入了新的關鍵字const來定義常量,const與let都具有塊級作用域:
'use strict';const PI = 3.14;PI = 3; // 某些瀏覽器不報錯,但是無效果!PI; // 3.14方法 在一個對象中綁定函數,稱為這個對象的方法。
在JavaScript中,對象的定義是這樣的:
var xiaoming = { name: '小明', birth: 1990};但是,如果我們給xiaoming綁定一個函數,就可以做更多的事情。比如,寫個age()方法,返回xiaoming的年齡:
var xiaoming = { name: '小明', birth: 1990, age: function () { var y = new Date().getFullYear(); return y - this.birth; }};xiaoming.age; // function xiaoming.age()xiaoming.age(); // 今年調用是25,明年調用就變成26了在一個方法內部,this是一個特殊變量,它始終指向當前對象,也就是xiaoming這個變量。所以,this.birth可以拿到xiaoming的birth屬性。
讓我們拆開寫:
function getAge() { var y = new Date().getFullYear(); return y - this.birth;}var xiaoming = { name: '小明', birth: 1990, age: getAge};xiaoming.age(); // 25, 正常結果getAge(); // NaN單獨調用函數getAge()怎么返回了NaN?請注意,我們已經進入到了JavaScript的一個大坑里。
JavaScript的函數內部如果調用了this,那么這個this到底指向誰?
答案是,視情況而定!
如果以對象的方法形式調用,比如xiaoming.age(),該函數的this指向被調用的對象,也就是xiaoming,這是符合我們預期的。
如果單獨調用函數,比如getAge(),此時,該函數的this指向全局對象,也就是window。
更坑爹的是,如果這么寫:
var fn = xiaoming.age; // 先拿到xiaoming的age函數fn(); // NaN也是不行的!要保證this指向正確,必須用obj.xxx()的形式調用!
由于這是一個巨大的設計錯誤,要想糾正可沒那么簡單。ECMA決定,在strict模式下讓函數的this指向undefined,因此,在strict模式下,你會得到一個錯誤:
'use strict';var xiaoming = { name: '小明', birth: 1990, age: function () { var y = new Date().getFullYear(); return y - this.birth; }};var fn = xiaoming.age;fn(); // Uncaught TypeError: Cannot read 這個決定只是讓錯誤及時暴露出來,并沒有解決this應該指向的正確位置。有些時候,喜歡重構的你把方法重構了一下:
'use strict';var xiaoming = { name: '小明', birth: 1990, age: function () { function getAgeFromBirth() { var y = new Date().getFullYear(); return y - this.birth; } return getAgeFromBirth(); }};xiaoming.age(); // Uncaught TypeError: Cannot read property 'birth' of undefined結果又報錯了!原因是this指針只在age方法的函數內指向xiaoming,在函數內部定義的函數,this又指向undefined了?。?strong>在非strict模式下,它重新指向全局對象window!)
修復的辦法也不是沒有,我們用一個that變量首先捕獲this:
'use strict';var xiaoming = { name: '小明', birth: 1990, age: function () { var that = this; // 在方法內部一開始就捕獲this function getAgeFromBirth() { var y = new Date().getFullYear(); return y - that.birth; // 用that而不是this } return getAgeFromBirth(); }};xiaoming.age(); // 25用var that = this;,你就可以放心地在方法內部定義其他函數,而不是把所有語句都堆到一個方法中。
apply
雖然在一個獨立的函數調用中,根據是否是strict模式,this指向undefined或window,不過,我們還是可以控制this的指向的!
要指定函數的this指向哪個對象,可以用函數本身的apply方法,它接收兩個參數,第一個參數就是需要綁定的this變量,第二個參數是Array,表示函數本身的參數。
(其實我個人決定就是告訴js調用函數的對象和參數而已)
用apply修復getAge()調用:
function getAge() { var y = new Date().getFullYear(); return y - this.birth;}var xiaoming = { name: '小明', birth: 1990, age: getAge};xiaoming.age(); // 25getAge.apply(xiaoming, []); // 25, this指向xiaoming, 參數為空另一個與apply()類似的方法是call(),唯一區別是:
apply()把參數打包成Array再傳入;
call()把參數按順序傳入。
比如調用Math.max(3, 5, 4),分別用apply()和call()實現如下:
Math.max.apply(null, [3, 5, 4]); // 5Math.max.call(null, 3, 5, 4); // 5對普通函數調用,我們通常把this綁定為null。
裝飾器
利用apply(),我們還可以動態改變函數的行為。
JavaScript的所有對象都是動態的,即使內置的函數,我們也可以重新指向新的函數。
現在假定我們想統計一下代碼一共調用了多少次parseInt(),可以把所有的調用都找出來,然后手動加上count += 1,不過這樣做太傻了。最佳方案是用我們自己的函數替換掉默認的parseInt():
var count = 0;var oldParseInt = parseInt; // 保存原函數window.parseInt = function () { count += 1; return oldParseInt.apply(null, arguments); // 調用原函數};// 測試:parseInt('10');parseInt('20');parseInt('30');count; // 3新聞熱點
疑難解答