當你聲明一個函數的時候,你就建立了一個做用域氣泡。前期的js只有函數能夠建立做用域,現代js引入了塊級做用域,每進入一次花括號{}就生成了一個塊級做用域。javascript
有些人會叫做用域鏈,可是我以爲這應該是個樹形結構。樹的頂層是全局做用域,在做用域內聲明函數會建立函數做用域子節點,遇{}會建立塊級做用域子節點,從而構成一顆樹html
function a{
if(true){
}
}
function b{
}複製代碼
如上代碼構成以下做用域樹
java
var、function聲明的變量依附最近的函數做用域或全局做用域,let、const聲明的變量依附於最近的塊級做用域、函數做用域或全局做用域,即全部做用域氣泡。c++
{
var a=1;
function b(){}
}
for(var c = 1;c<=1;c++){
var d =1;
function e(){}
}
if(true){
var f=1;
function g(){}
}
function h (){
var i =1;
}
// a,b,c,d,e,f,g,h上層無函數聲明,依附全局做用域,均可以訪問到
console.log(a);
console.log(b);
console.log(c);
console.log(d);
console.log(e);
console.log(f);
console.log(h);
// i 依附h函數做用域,沒法訪問到
console.log(i);//reference error
{
let j=1;
const k=1;
}
for(let l = 1;l<=1;l++){
let m =1;
}
if(true){
let n=1;
}
function p (){
let o =1;
}
// 依附最近的做用域,沒法訪問到
console.log(j);//reference error
console.log(k);//reference error
console.log(l);//reference error
console.log(m);//reference error
console.log(n);//reference error
console.log(o);//reference error複製代碼
在當前做用域往上查找,找到第一個匹配的標識符時中止返回變量值,若是查到全局做用域都沒有,則拋出reference error錯誤,所以變量值查找有就近原則。es6
javascript 代碼執行前要通過編譯環節,該環節會生成部分做用域樹(有些是動態生成),以下代碼bash
console.log(a);// 輸出a的函數定義
function a() {
console.log(b);// undefined
var b = 2;
console.log(b);// 2
}複製代碼
通過編譯環節後,等價於babel
function a() {
var b;
console.log(b);
b = 2;
console.log(b);
}
console.log(a);複製代碼
只有function和var這種變量聲明會變量提高,而let,const不會。變量提高的時候,函數聲明會首先被提高,而後纔是變量。(個人記憶方法是函數的第一公民,權利最大)閉包
foo(); // 1
function foo() {
console.log( 1 );
}
var foo = function() {
console.log( 2 );
};
foo();// 2複製代碼
通過編譯環節後異步
function foo() {
console.log( 1 );
}
var foo;// 已經聲明過,被忽略
foo(); // 1
foo = function() { console.log( 2 );};
foo(); // 2複製代碼
const a;//報錯,沒有賦值初值
const b = {a:1};
b.a = 2;//沒錯,變量指向的內容能夠變
b = {a:2}//出錯,變量的指向不能變複製代碼
這塊內容在筆試題上反映吧
詳細看這篇博客函數
塊級做用域是es6纔有的東西,以前的javascript引擎並不支持,因而就有了babel編譯神器,將es6的代碼編譯成低版本javascript引擎能正確執行的代碼。那babel是怎麼實現塊級做用域的呢,答案就是閉包和變量更名。
第一題:
console.log(a());// 2
var a = function b(){
console.log(1);
}
console.log(a());// 1
function a(){
console.log(2);
}
console.log(a());// 1
console.log(b());// reference error複製代碼
代碼編譯後,變量提高,函數優先,賦值語句中b爲右值,非變量聲明,因此代碼等價於
function a(){
console.log(2);
}
var a;
console.log(a());// 2
a = function (){
console.log(1);
}
console.log(a());// 1
console.log(a());// 1
console.log(b());// reference error複製代碼
第二題:
function test() {
console.log(a);// undefined
console.log(b);// reference error
console.log(c);// reference error
var a = b =1;// 等價於 var a=1;b=1;
let c = 1;
}
test();
console.log(b);// 1
console.log(a);// reference error複製代碼
var 聲明的變量會變量提高,並依附函數做用域,而,b變量未聲明就做爲左值,會變成全局變量,可是不會變量提高;
第三題:
"use strict";
function test() {
console.log(a);// undefined
console.log(b);// reference error
console.log(c);// reference error
var a = b =1;// 直接拋出語法錯誤
let c = 1;
}
test();
console.log(b);// reference error
console.log(a);// reference error複製代碼
進入嚴格模式後,b=1這種語法會直接出錯,不會變成全局變量
第四題:
4.1題
for(var i=0;i<5;i++){
setTimeout(function(){console.log(i)},0); // 5 5 5 5 5
}複製代碼
i 依附函數做用域,執行過程只有一個i,而setTimeout是異步函數,須要等棧中的代碼執行完後再執行,此時i已經變爲5
4.2題
for(let i=0;i<5;i++){
setTimeout(function(){console.log(i)},0); // 1 2 3 4
}複製代碼
let 依附for的塊級做用域,代碼等價於
for(let i=0;i<5;i++){
let j = i;
setTimeout(function(){console.log(j)},0); // 1 2 3 4
}複製代碼
能夠看出每次循環都產生一個新的內存單元,異步函數執行時,取到的值爲當時保持的快照值。
4.3題
for(var i=0;i<5;i++){
(function(i){
setTimeout(function(){console.log(i)},0); // 1 2 3 4
})(i);
}複製代碼
使用IIFE來建立快照值,將for 循環的i 傳遞給當即執行表達式中的參數i,i是基本數據類型,爲賦值傳遞,會產生一個新的內存單元,每次循環都產生一個快照值,因此異步函數執行的時候,取到的值爲當時保持的快照值。
4.4題
for(var i={j:0};i.j<5;i.j++){
(function(i){
setTimeout(function(){console.log(i.j)},0); // 5 5 5 5 5
})(i);
}複製代碼
i不是基本數據類型,爲引用傳遞,i始終指向同一內存單元
4.5題
for(let i={j:0};i.j<5;i.j++){
setTimeout(function(){console.log(i.j)},0); // 5 5 5 5 5
}複製代碼
與上面4.4題同理
4.6題
如何改變4.4題的當即執行表達式,輸出1,2,3,4
for(var i={j:0};i.j<5;i.j++){
(function(i){
setTimeout(function(){console.log(i.j)},0); // 1 2 3 4
})(JSON.parse(JSON.stringify(i)));
}複製代碼
將i序列化和反序列化,產生新的內存單元值,不讓i指向同一內存單元