JavaScript學習記錄day6-函數變量作用域、解構賦值與方法
@(學習)[javascript]
[TOC]
1. 作用域
在JavaScript中,用var
申明的變量實際上是有作用域的。
如果一個變量在函數體內部申明,則該變量的作用域為整個函數體,在函數體外不可引用該變量:
‘use strict‘;
function foo() {
var x = 1;
x = x + 1;
}
x = x + 2; // ReferenceError: x is not defined 無法在函數體外引用變量x
如果兩個不同的函數各自申明了同一個變量,那麽該變量只在各自的函數體內起作用。換句話說,不同函數內部的同名變量互相獨立,互不影響:
function foo() {
var x = 1;
x = x + 1;
console.log(x);
}
function bar() {
var x = ‘A‘;
x = x + ‘B‘;
console.log(x);
}
foo(); // 2
bar(); // AB
由於JavaScript的函數可以嵌套,此時,內部函數可以訪問外部函數定義的變量,反過來則不行:
function foo() { var x = 1; function bar() { var y = x + 1; // bar可以訪問foo的變量x } var z = y + 1; // ReferenceError: y is not defined, foo不可以訪問bar的變量y } foo();
如果內部函數和外部函數的變量名重名怎麽辦?來測試一下:
‘user strict‘;
function foo() {
var x = 1;
function bar() {
var x = ‘A‘;
console.log(‘x in bar() = ‘ + x);
}
console.log(‘x in foo() = ‘ + x);
bar();
}
foo();
// 結果:
// x in foo() = 1
// x in bar() = A
這說明JavaScript的函數在查找變量時從自身函數定義開始,從“內”向“外”查找。如果內部函數定義了與外部函數重名的變量,則內部函數的變量將“屏蔽”外部函數的變量。
2. 變量提升
JavaScript的函數定義有個特點,它會先掃描整個函數體的語句,把所有申明的變量“提升”到函數頂部:
‘use strict‘;
function foo() {
var x = ‘Hello, ‘ + y;
console.log(x);
var y = ‘Bob‘;
}
foo();
雖然是strict
模式,但語句var x = ‘Hello, ‘ + y;
並不報錯,原因是變量y
在稍後申明了。但是console.log
顯示Hello, undefined
,說明變量y
的值為undefined
。這正是因為JavaScript引擎自動提升了變量y的聲明,但不會提升變量y
的賦值。
對於上述foo()
函數,JavaScript引擎看到的代碼相當於:
function foo() {
var y; // 提升變量y的申明,此時y為undefined
var x = ‘Hello, ‘ + y;
console.log(x);
y = ‘Bob‘;
}
由於JavaScript的這一怪異的“特性”,我們在函數內部定義變量時,請嚴格遵守“在函數內部首先申明所有變量”這一規則。最常見的做法是用一個var申明函數內部用到的所有變量:
function foo() {
var
x = 1, // x初始化為1
y = x + 1, // y初始化為2
z, i; // z和i為undefined
// 其他語句:
for (i=0; i<100; i++) {
...
}
}
3. 全局作用域
不在任何函數內定義的變量就具有全局作用域。實際上,JavaScript默認有一個全局對象window
,全局作用域的變量實際上被綁定到window
的一個屬性:
‘use strict‘;
var course = ‘Learn JavaScript‘;
console.log(course); // ‘Learn JavaScript‘
console.log(window.course); // ‘Learn JavaScript‘
因此,直接訪問全局變量course
和訪問window.course
是完全一樣的。
由於函數定義有兩種方式,以變量方式var foo = function () {}
定義的函數實際上也是一個全局變量,因此,頂層函數的定義也被視為一個全局變量,並綁定到window
對象:
‘use strict‘;
function foo() {
alert(‘foo‘);
}
foo(); // 直接調用foo()
window.foo(); // 通過window.foo()調用
我們每次直接調用的alert()函數其實也是window的一個變量:
‘use strict‘;
window.alert(‘調用window.alert()‘);
// 把alert保存到另一個變量:
var old_alert = window.alert;
// 給alert賦一個新函數:
window.alert = function () {}
alert(‘無法用alert()顯示了!‘);
// 恢復alert:
window.alert = old_alert;
alert(‘又可以用alert()了!‘);
這說明JavaScript實際上只有一個全局作用域。任何變量(函數也視為變量),如果沒有在當前函數作用域中找到,就會繼續往上查找,最後如果在全局作用域中也沒有找到,則報ReferenceError
錯誤。
4. 名字空間
全局變量會綁定到window
上,不同的JavaScript文件如果使用了相同的全局變量,或者定義了相同名字的頂層函數,都會造成命名沖突,並且很難被發現。
減少沖突的一個方法是把自己的所有變量和函數全部綁定到一個全局變量中。例如:
// 唯一的全局變量MYAPP:
var MYAPP = {};
// 其他變量:
MYAPP.name = ‘myapp‘;
MYAPP.version = 1.0;
// 其他函數:
MYAPP.foo = function () {
return ‘foo‘;
};
把自己的代碼全部放入唯一的名字空間MYAPP
中,會大大減少全局變量沖突的可能。
許多著名的JavaScript庫都是這麽幹的:jQuery,YUI,underscore等等。
5. 局部作用域
由於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;
}
// SyntaxError:
i += 1;
}
6. 常量
由於var
和let
申明的是變量,如果要申明一個常量,在ES6之前是不行的,我們通常用全部大寫的變量來表示“這是一個常量,不要修改它的值”:
var PI = 3.14;
ES6標準引入了新的關鍵字const
來定義常量,const
與let
都具有塊級作用域:
‘use strict‘;
const PI = 3.14;
PI = 3; // 某些瀏覽器不報錯,但是無效果!
PI; // 3.14
7. 解構賦值
從ES6開始,JavaScript引入了解構賦值,可以同時對一組變量進行賦值。
什麽是解構賦值?我們先看看傳統的做法,如何把一個數組的元素分別賦值給幾個變量:
var array = [‘hello‘, ‘JavaScript‘, ‘ES6‘];
var x = array[0];
var y = array[1];
var z = array[2];
現在,在ES6中,可以使用解構賦值,直接對多個變量同時賦值:
‘use strict‘;
// 如果瀏覽器支持解構賦值就不會報錯:
var [x, y, z] = [‘hello‘, ‘JavaScript‘, ‘ES6‘];
// x, y, z分別被賦值為數組對應元素:
console.log(‘x = ‘ + x + ‘, y = ‘ + y + ‘, z = ‘ + z);
<font color="red">註意</font>:
對數組元素進行解構賦值時,多個變量要用[...]
括起來。
如果數組本身還有嵌套,也可以通過下面的形式進行解構賦值,註意嵌套層次和位置要保持一致:
let [x, [y, z]] = [‘hello‘, [‘JavaScript‘, ‘ES6‘]];
x; // ‘hello‘
y; // ‘JavaScript‘
z; // ‘ES6‘
解構賦值還可以忽略某些元素:
let [, , z] = [‘hello‘, ‘JavaScript‘, ‘ES6‘]; // 忽略前兩個元素,只對z賦值第三個元素
z; // ‘ES6‘
如果需要從一個對象中取出若幹屬性,也可以使用解構賦值,便於快速獲取對象的指定屬性:
‘use strict‘;
var person = {
name: ‘小明‘,
age: 20,
gender: ‘male‘,
passport: ‘G-12345678‘,
school: ‘No.4 middle school‘
};
var {name, age, passport} = person;
// name, age, passport分別被賦值為對應屬性:
console.log(‘name = ‘ + name + ‘, age = ‘ + age + ‘, passport = ‘ + passport);
有些時候,如果變量已經被聲明了,再次賦值的時候,正確的寫法也會報語法錯誤:
// 聲明變量:
var x, y;
// 解構賦值:
{x, y} = { name: ‘小明‘, x: 100, y: 200};
// 語法錯誤: Uncaught SyntaxError: Unexpected token =
這是因為JavaScript引擎把{開頭的語句當作了塊處理,於是=不再合法。解決方法是用小括號括起來:
({x, y} = { name: ‘小明‘, x: 100, y: 200});
解構賦值使用場景
解構賦值在很多時候可以大大簡化代碼。例如,交換兩個變量x
和y
的值,可以這麽寫,不再需要臨時變量:
var x=1, y=2;
[x, y] = [y, x]
快速獲取當前頁面的域名和路徑:
var {hostname:domain, pathname:path} = location;
如果一個函數接收一個對象作為參數,那麽,可以使用解構直接把對象的屬性綁定到變量中。例如,下面的函數可以快速創建一個Date
對象:
function buildDate({year, month, day, hour=0, minute=0, second=0}) {
return new Date(year + ‘-‘ + month + ‘-‘ + day + ‘ ‘ + hour + ‘:‘ + minute + ‘:‘ + second);
}
它的方便之處在於傳入的對象只需要year
、month
和day
這三個屬性:
buildDate({ year: 2017, month: 1, day: 1 });
// 2017-12-31T16:00:00.000Z (UTC)
也可以傳入hour
、minute
和second
屬性:
buildDate({ year: 2018, month: 1, day: 1, hour: 20, minute: 15 });
// 2018-01-01T12:15:00.000Z (UTC)
使用解構賦值可以減少代碼量,但是,需要在支持ES6解構賦值特性的現代瀏覽器中才能正常運行。目前支持解構賦值的瀏覽器包括Chrome,Firefox,Edge等。
8. 方法
在一個對象中綁定函數,稱為這個對象的方法。
在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;
}
};
console.log(xiaoming.age); // [Function: age]
console.log(xiaoming.age()); // 當前調用是28,下一年調用就變成29了
綁定到對象上的函數稱為方法,和普通函數也沒啥區別,但是它在內部使用了一個this
關鍵字。
在一個方法內部,this
是一個特殊變量,它始終指向當前對象,也就是xiaoming
這個變量。所以,this.birth
可以拿到xiaoming
的birth
屬性。
讓我們拆開寫:
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}
var xiaoming = {
name: ‘小明‘,
birth: 1990,
age: getAge
};
console.log(xiaoming.age()); // 25, 正常結果
console.log(getAge()); // NaN
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(); // TypeError: Cannot read property ‘birth‘ of undefined
這個決定只是讓錯誤及時暴露出來,並沒有解決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(); // TypeError: Cannot read property ‘birth‘ of undefined
結果又報錯了!原因是this
指針只在age
方法的函數內指向xiaoming
,在函數內部定義的函數,this
又指向undefined
了!(在非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();
}
};
console.log(xiaoming.age()); // 28
用var that = this;
,你就可以放心地在方法內部定義其他函數,而不是把所有語句都堆到一個方法中。
9. apply
雖然在一個獨立的函數調用中,根據是否是strict
模式,this
指向undefined
或window
,不過,我們還是可以控制this
的指向的!
要指定函數的this
指向哪個對象,可以用函數本身的apply
方法,它接收兩個參數,第一個參數就是需要綁定的this
變量,第二個參數是Array
,表示函數本身的參數。
用apply
修復getAge()
調用:
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}
var xiaoming = {
name: ‘小明‘,
birth: 1990,
age: getAge
};
console.log(xiaoming.age()); // 28
console.log(getAge.apply(xiaoming, [])); // 28, this指向xiaoming, 參數為空
另一個與apply()
類似的方法是call()
,唯一區別是:
apply()
把參數打包成Array
再傳入;
call()
把參數按順序傳入。
比如調用Math.max(3, 5, 4)
,分別用apply()
和call()
實現如下:
console.log(Math.max.apply(null, [3, 5, 4])); // 5
console.log(Math.max.call(null, 3, 5, 4)); // 5
對普通函數調用,我們通常把this
綁定為null
。
10. 裝飾器
利用apply()
,我們還可以動態改變函數的行為。
JavaScript的所有對象都是動態的,即使內置的函數,我們也可以重新指向新的函數。
現在假定我們想統計一下代碼一共調用了多少次parseInt()
,可以把所有的調用都找出來,然後手動加上count += 1
,不過這樣做太傻了。最佳方案是用我們自己的函數替換掉默認的parseInt()
:
‘use strict‘;
var count = 0;
var oldParseInt = parseInt; // 保存原函數
function parseInt() {
}
window.parseInt = function () {
count += 1;
return oldParseInt.apply(null, arguments); // 調用原函數
};
// 測試:
parseInt(‘10‘);
parseInt(‘20‘);
parseInt(‘30‘);
console.log(‘count = ‘ + count); // 3
學習參考教程:http://www.liaoxuefeng.com
JavaScript學習記錄day6-函數變量作用域、解構賦值與方法