內容來自廖雪峯大神官網:https://www.liaoxuefeng.com/wiki/1022910821149312/1023021187855808
在JavaScript中,用var
申明的變量實際上是有作用域的。
如果一個變量在函數體內部申明,則該變量的作用域爲整個函數體,在函數體外不可引用該變量:
'use strict';
function foo() {
var x = 1;
x = x + 1;
}
x = x + 2; // ReferenceError! 無法在函數體外引用變量x
如果兩個不同的函數各自申明瞭同一個變量,那麼該變量只在各自的函數體內起作用。換句話說,不同函數內部的同名變量互相獨立,互不影響:
'use strict';
function foo() {
var x = 1;
x = x + 1;
}
function bar() {
var x = 'A';
x = x + 'B';
}
由於JavaScript的函數可以嵌套,此時,內部函數可以訪問外部函數定義的變量,反過來則不行:
'use strict';
function foo() {
var x = 1;
function bar() {
var y = x + 1; // bar可以訪問foo的變量x!
}
var z = y + 1; // ReferenceError! foo不可以訪問bar的變量y!
}
這說明JavaScript的函數在查找變量時從自身函數定義開始,從“內”向“外”查找。如果內部函數定義了與外部函數重名的變量,則內部函數的變量將“屏蔽”外部函數的變量。
變量提升
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++) {
...
}
}
全局作用域
不在任何函數內定義的變量就具有全局作用域。實際上,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
對象:
'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: window.alert = old_alert; alert('又可以用alert()了!');
Run
這說明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;
}
// SyntaxError:
i += 1;
}
常量
由於var
和let
申明的是變量,如果要申明一個常量,在ES6之前是不行的,我們通常用全部大寫的變量來表示“這是一個常量,不要修改它的值”:
var PI = 3.14;
ES6標準引入了新的關鍵字const
來定義常量,const
與let
都具有塊級作用域:
'use strict';
const PI = 3.14;
PI = 3; // 某些瀏覽器不報錯,但是無效果!
PI; // 3.14
解構賦值
從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'];
Run
注意,對數組元素進行解構賦值時,多個變量要用[...]
括起來。
如果數組本身還有嵌套,也可以通過下面的形式進行解構賦值,注意嵌套層次和位置要保持一致:
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;
Run
對一個對象進行解構賦值時,同樣可以直接對嵌套的對象屬性進行賦值,只要保證對應的層次是一致的:
var person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678',
school: 'No.4 middle school',
address: {
city: 'Beijing',
street: 'No.1 Road',
zipcode: '100001'
}
};
var {name, address: {city, zip}} = person;
name; // '小明'
city; // 'Beijing'
zip; // undefined, 因爲屬性名是zipcode而不是zip
// 注意: address不是變量,而是爲了讓city和zip獲得嵌套的address對象的屬性:
address; // Uncaught ReferenceError: address is not defined
使用解構賦值對對象屬性進行賦值時,如果對應的屬性不存在,變量將被賦值爲undefined
,這和引用一個不存在的屬性獲得undefined
是一致的。如果要使用的變量名和屬性名不一致,可以用下面的語法獲取:
var person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678',
school: 'No.4 middle school'
};
// 把passport屬性賦值給變量id:
let {name, passport:id} = person;
name; // '小明'
id; // 'G-12345678'
// 注意: passport不是變量,而是爲了讓變量id獲得passport屬性:
passport; // Uncaught ReferenceError: passport is not defined
解構賦值還可以使用默認值,這樣就避免了不存在的屬性返回undefined
的問題:
var person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678'
};
// 如果person對象沒有single屬性,默認賦值爲true:
var {name, single=true} = person;
name; // '小明'
single; // true
有些時候,如果變量已經被聲明瞭,再次賦值的時候,正確的寫法也會報語法錯誤:
// 聲明變量:
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 });
// Sun Jan 01 2017 00:00:00 GMT+0800 (CST)
也可以傳入hour
、minute
和second
屬性:
buildDate({ year: 2017, month: 1, day: 1, hour: 20, minute: 15 });
// Sun Jan 01 2017 20:15:00 GMT+0800 (CST)
使用解構賦值可以減少代碼量,但是,需要在支持ES6解構賦值特性的現代瀏覽器中才能正常運行。目前支持解構賦值的瀏覽器包括Chrome,Firefox,Edge等。