做用域,是指變量的生命週期。javascript
一、全局做用域前端
全局變量:生命週期將存在於整個程序以內。能被程序中任何函數或者方法訪問。在javascript內默認是能夠被修改的。java
1.1顯示聲明node
帶有關鍵字var的聲明:面試
var a = 1;
var f = function(){
console.log("come on");
};
//全局變量會掛在到window對象上
console.log(window.a);
console.log(window.f);複製代碼
1.2隱式聲明編程
function f(num){
result = num + 1;
return result;
}
f(1);
console.log(window.result);複製代碼
不帶有var關鍵字的變量result,也被隱式聲明爲一個全局變量。掛載在window對象上。數組
二、函數做用域瀏覽器
function f(){
var a = 1;
}
console.log(a);複製代碼
2.1如何訪問函數做用域內的變量呢?bash
法一:經過return返問函數內部變量閉包
function f(num){
var result = num++;
return result;
}
console.log(f(1));複製代碼
function f(num){
var result = num + 1;
return result;
}
console.log(f(1));複製代碼
function f(num){
var result = ++num;
return result;
}
console.log(f(1));複製代碼
以上三段程序也體現了i++和++i的區別。
法2、經過閉包訪問函數內部的變量
function outer(){
var value = 'inner';
return function inner(){
return value;
}
}
console.log(outer()());複製代碼
2.2當即執行函數
當即執行函數可以自動執行(function(){})()裏面包裹的內容,可以很好地消除全局變量的影響。
(function(){
var a = 1;
var foo = function(){
console.log("haha");
}
})();
console.log(window.a);
console.log(window.foo);複製代碼
三、塊級做用域
在 ES6 以前,是沒有塊級做用域的概念的。
for(var i = 0; i < 3; i++){
}
console.log(i); 複製代碼
很明顯,用 var 關鍵字聲明的變量,存在變量提高,至關於:
var i;
for(i = 0; i < 3; i++){
}
console.log(i); 複製代碼
若是須要實現塊級做用域,可使用let關鍵字,let關鍵字是不存在變量提高的。
for(let i = 0; i < 3; i++){
}
console.log(i);
複製代碼
一樣能造成塊級做用域的還有const關鍵字。
if(true){
const a = 1;
}
console.log(a);複製代碼
塊級做用域的做用以及常考的面試題
for(var i = 0; i < 3; i++){
setTimeout(function(){
console.log(i);
},200);
}複製代碼
爲何i是3呢?
緣由由於var聲明的變量能夠進行變量提高,i是在全局做用域裏面的,for()循環是同步函數,setTimeout是異步操做,異步操做必須等到全部的同步操做執行完畢後才能執行,執行異步操做以前i已是3,因此以後會輸出同一個值3。
如何讓它按咱們想要的結果輸出呢?
法一:最簡單使用let
for(let i = 0; i < 3; i++){
setTimeout(function(){
console.log(i);
},200);
}
複製代碼
法二:調用函數,建立函數做用域;
for(var i = 0; i < 3; i++){
f(i);
}
function f(i){
setTimeout(function(){
console.log(i);
},200);
}複製代碼
法3、當即執行函數
for(var i = 0; i < 3; i++){
(function(j){
setTimeout(function(){
console.log(j);
},200)
})(i);
}複製代碼
當即執行函數同函數調用,先把for循環中的i記錄下來,而後把i賦值給j,而後輸出0,1,2。
四、詞法做用域
函數的做用域在函數定義的時候就決定了。
var value = 'outer';
function foo(){
var value = 'middle';
console.log(value); //middle
function bar(){
var value = 'inner';
console.log(value); //innner
}
return bar();
}
foo();
console.log(value); //outer複製代碼
當咱們要使用聲明的變量時:JS引擎總會從最近的一個域,向外層域查找;
例:面試題
var a = 2;
function foo(){
console.log(a);
}
function bar(){
var a = 3;
foo();
}
bar();複製代碼
若是是詞法做用域,也就是如今的javascript環境。變量a首先會在foo()函數裏面查找,若是沒有找到,會根據書寫的位置,查找上一層的代碼,在這裏是全局做用域,找到並賦值爲2,因此控制檯輸出2。
咱們說過,詞法做用域是寫代碼的時候就靜態肯定下來的。Javascript中的做用域就是詞法做用域(事實上大部分語言都是基於詞法做用域的),因此這段代碼在瀏覽器中運行的結果是輸出 2
。
做用域的"遮蔽"
做用域查找從運行時所處的最內部做用域開始,逐級向外或者說向上進行,直到碰見第一個匹配的標識符爲止。在多層的嵌套做用域中能夠定義同名的標識符,這叫做「遮蔽效應」,內部的標識符「遮蔽」了外部的標識符。
var a = 0;
function test(){
var a = 1;
console.log(a);//1
}
test();複製代碼
var a = 0;
function test(){
var a = 1;
console.log(window.a);//0
}
test();複製代碼
經過這種技術能夠訪問那些被同名變量所遮蔽的全局變量。但非全局的變量若是被遮蔽了,不管如何都沒法被訪問到。
五、動態做用域
而動態做用域並不關心函數和做用域是如何聲明以及在何處聲明的,只關心它們從何處調用。
動態做用域,做用域是基於調用棧的,而不是代碼中的做用域嵌套;
請聽面試題:
var x = 3;
var y = 3;
function A(y){
var x = 2;
var num = 3;
num++; //4
function B(num){
return x * y * num; //x,y,num:1,5,4
}
x = 1;
return B;
}
console.log(A(5)(4));
複製代碼
解析:
本題的關鍵在於肯定x的值,函數B是在 return B;時執行的,因此x的值在函數調用前已經修改成1;因此返回20。
2、做用域鏈
每個 javaScript 函數都表示爲一個對象,更確切地說,是 Function 對象的一個實例。
Function 對象同其餘對象同樣,擁有可編程訪問的屬性。和一系列不能經過代碼訪問的屬性,而這些屬性是提供給 JavaScript 引擎存取的內部屬性。其中一個屬性是 [[Scope]] ,由 ECMA-262標準第三版定義。
內部屬性 [[Scope]] 包含了一個函數被建立的做用域中對象的集合。
這個集合被稱爲函數的 做用域鏈,它能決定哪些數據能被訪問到。
來源於:《 高性能JavaScript 》;
例:
function add(x,y){
return x + y;
}
console.log(add.prototype);
複製代碼
[[Scope]]
屬性下是一個數組,裏面保存了,做用域鏈,此時只有一個 global
。
理解詞法做用域的原理
var a = 2;
function foo(){
console.log(a); //2
console.log(foo.prototype);
}
function bar(){
var a = 3;
console.log(a); //3
foo();
console.log(bar.prototype);
}
bar();複製代碼
node環境下:
瀏覽器下:
疑惑:爲何在node中scopes數組中有兩個對象,在瀏覽器中scopes數組中只有一個對象。
緣由:node的模塊化,本質上也是在外層添加一個匿名函數,由於node的模塊化,在編譯的時候,給每一個JS文件外部包裹一個匿名函數。因此會出現scopes中有兩個對象。展開scopes[0],會發現裏面確實包含在瀏覽器中是全局的變量或全局的函數,而在node環境下因爲多包裹了一層匿名函數,會讓它存在於closure中。
var a = 2;
function bar(){
var a = 3;
console.log(a); //3
foo();
console.log(bar.prototype);
function foo(){
console.log(a); //3
console.log(foo.prototype);
}
}
bar();複製代碼
node環境下:
瀏覽器下:
全局做用域鏈是在全局執行上下文初始化時就已經肯定了。
證實:
console.log(add.prototype); //1聲明前
function add(x,y){
console.log(add.prototype); //2運行時
return x + y;
}
add(1,2);
console.log(add.prototype); //3執行後
複製代碼
1聲明前
2運行時
3執行後
做用域鏈是在 JS 引擎完成初始化執行上下文環境就已經肯定了。
理解做用域鏈的好處:若是做用域鏈越深, [0] => [1] => [2] => [...] => [n],咱們調用的是全局變量,它永遠在最後一個(這裏是第 n 個),這樣的查找到咱們須要的變量會引起多大的性能問題,因此,儘可能將 全局變量局部化 ,避免做用域鏈的層層嵌套。
理解執行上下文
[[Scope]]
屬性的做用域鏈裏面已經有以上內容。this
arguments
以及咱們聲明的變量,這個例子裏面是 x
、y
。結束執行上下文階段
做用域鏈和執行上下文的關係
做用域鏈本質上是指向一個指針,指向變量對象列表。建立函數時,會把全局變量對象的做用域鏈添加在[[Scope]]屬性中。調用函數時,會爲函數建立一個執行環境的做用域鏈,而且建立一個活動對象,並將其推入執行環境做用域鏈的前端。函數局部環境的變量對象只有在函數執行的過程當中才存在。通常來說,當函數執行完畢後,局部活動對象就會被銷燬,內存中僅保存全局做用域。可是閉包的狀況有所不一樣。
3、閉包
function createComparisonFunction(propertyName){
return function(object1,object2){
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if(value1 < value2){
return -1;
}else if(value1 > value2){
return 1;
}else{
return 0;
}
}
}
//建立函數
var compareNames = createComparisonFunction('name');
//調用函數
var result = compareNames({name:"Nicholas"},{name:"Greg"});
//解除對匿名函數的引用
compareNames = null;複製代碼
調用compareNames()
函數的過程當中產生的做用域鏈之間的關係圖以下
createComparisonFunction執行完畢後,其執行環境的做用域鏈會被銷燬,但其活動對象不會被銷燬,由於匿名函數的做用域鏈還在引用這個活動對象。直到匿名函數被銷燬後,createComparisonFunction()的活動對象纔會被銷燬。
閉包與變量
function createFunctions(){
var result = new Array();
for(var i = 0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}
console.log(createFunctions()[0]()); //10複製代碼
實際上每一個函數都會返回10。緣由:由於每一個函數的做用域鏈中都會保存着createFunctions()函數的活動對象,因此它們引用的都是同一個變量。
如何讓閉包輸出想要的結果呢?
function createFunctions(){
var result = new Array();
for(var i = 0; i < 10; i++){
result[i] = function(num){
return function(){
return num;
}
}(i);
}
return result;
}
for(var j = 0; j < 10; j++){
console.log(createFunctions()[j]());
}複製代碼
理解:咱們沒有直接把閉包賦值給數組,而是定義了一個匿名函數,而後給該匿名函數傳入參數,因爲參數是按值傳遞的,因此至關於把當前的i賦值給num,再在該匿名函數內生成一個閉包,返回num。