最近被一道面試題給難住了,其實就是說不清楚爲何是這個答案,有時候可能屏幕前的你,也會有這個疑惑,因此打算來補一補基礎-做用域。javascript
爲了更好的理解它,能夠看看往期的知識點:java
先上題目:github
var Fn = function () {
console.log(Fn);
}
Fn();
var obj = {
fn2 : function () {
console.log(fn2);
}
}
obj.fn2();
複製代碼
個人答案認爲兩個都是打印Function,其實基礎紮實的小夥伴估計明白我錯哪了。面試
話很少說,開始咱們的正題吧🤭安全
那麼,就有人問了,做用域規則在哪裏,如何被設置呢?性能優化
官方給出解釋:點這裏閉包
那麼我在這裏就不咬文嚼字了,那麼咱們要探究的就是靜態的問題了🤭函數
由於 JavaScript 採用的是詞法做用域,函數的做用域在函數定義的時候就決定了。工具
而與詞法做用域相對的是動態做用域,函數的做用域是在函數調用的時候才決定的。
讓咱們認真看個例子就能明白之間的區別:
var x = 10;
function fn() {
console.log(x);
}
fn()
function show(fun) {
var x = 20;
fun()
}
show(fn);
複製代碼
假設JavaScript採用靜態做用域,讓咱們分析下執行過程:
執行fn函數,先從fn函數內部查找是否有局部變量x,若是沒有,就根據書寫的位置,查找上面一層的代碼,也就是 value 等於 1,因此結果會打印 1。
假設JavaScript採用靜態做用域,讓咱們分析下執行過程:
執行 fn函數,依然是從 fn 函數內部查找是否有局部變量 x。若是沒有,就從調用函數的做用域,也就是 show函數內部查找 x變量,因此結果會打印 2。
實際JavaScript打印的結果就是1,從結果上說明JavaScript是靜態做用域。
爲了更好的理解,經過畫一張簡單圖來理解靜態做用域:
這樣子就很好理解這個關係了,Fn函數在本身的做用域中找變量x,根據變量查找規則,若是沒有的話,會去上一級的做用域查找,也就是全局做用域,看是否存在變量x,有的話就取這個值,沒有的話就返回undefined。
**一旦找到第一個匹配,做用域查詢就中止了。**相同的標識符名稱能夠在嵌套做用域的多個層中被指定,這稱爲「遮蔽(shadowing)」(內部的標識符「遮蔽」了外部的標識符)。
上述這個查詢的過程,叫作做用域查詢,它老是從當前被執行的最內側的做用域開始,向外/向上不斷查找,直到第一個匹配才中止。
在代碼任何地方都能訪問到的對象擁有全局做用域,更深刻的瞭解能夠結合全局執行上下文。好比: JavaScript的全局對象 函數 變量都能在全局訪問到。
var demo = 1; //全局變量
let fn = () => {
alert(demo)
let inner = () => alert(demo);
}
fn(); //1
inner() //ReferenceError
複製代碼
var demo = 1; //全局變量
let fn = () => {
demo1 = '未使用var定義'
alert(demo)
let inner = () => alert(demo1);
}
fn(); //1
console.log(window.demo1); //未使用var定義
複製代碼
和全局做用域相反,函數做用域通常只在函數的代碼片斷內可訪問到,外部不能進行變量訪問。在函數內部定義的變量存在於函數做用域中,其生命週期隨着函數的執行結束而結束。例如:
let name = '李四'
let getName = () => {
var name = '張三';
alert(name); //張三
}
console.log(name); //李四
複製代碼
在ES6中提出塊級做用域概念,它的用途就是:變量的聲明應該距離使用的地方越近越好。並最大限度的本地化。避免污染。
塊做用域由 { } 包括,let const能夠造成塊級做用域,也就是俗稱的暫時性死區。具體的在這裏就不詳細的介紹了,感興趣的能夠了解下以前的文章-JavaScript執行上下文-執行棧 這裏面講了爲何let const 會存在暫時性死區,原理是什麼?
與詞法做用域不一樣於在定義時肯定,動態做用域在執行時肯定,其生存週期到代碼片斷執行爲止。動態變量存在於動態做用域中,任何給定的綁定的值,在肯定調用其函數以前,都是不可知的。
從某種程度上來講,這會修改做用域,(也就是欺騙)詞法做用域。在你的代碼中建議不要使用它們,這是由於在某些方面: 欺騙詞法做用域會致使更低下的性能。
JavaScript中的eval(..)
函數接收一個字符串做爲參數值,並將這個字符串的內容看做是好像它已經被實際編寫在程序的那個位置上。
在eval(..)
被執行的後續代碼行中,引擎 將不會「知道」或「關心」前面的代碼是被動態翻譯的,並且所以修改了詞法做用域環境。引擎 將會像它一直作的那樣,簡單地進行詞法做用域查詢。
考慮下面代碼:
var b = 2;
function demo(str, a) {
eval(str); // 欺騙詞法做用域
console.log(a, b);
}
demo("var b = 12;", 1); // 1, 12
複製代碼
在eval(..)
調用的位置上,字符串"var b = 12"
被看做是一直就存在第2行的代碼。由於這個代碼恰巧聲明瞭一個新的變量b
,它就修改了現存的demo(..)
的詞法做用域。事實上,就像上面提到的那樣,這個代碼實際上在demo(..)
內部建立了變量b
,它遮蔽了聲明在外部(全局)做用域中的b
。
當console.log(..)
調用發生時,它會在demo(..)
的做用域中找到a
和b
,並且毫不會找到外部的b
。這樣,咱們就打印出"1, 12"而不是通常狀況下的"1, 2"。
假設:eval(..)
執行的代碼字符串包含一個或多個聲明(變量或函數)的話,這個動做就會修改這個eval(..)
所在的詞法做用域。技術上講,eval(..)
能夠經過種種技巧(超出了咱們這裏的討論範圍)被「間接」調用,而使它在全局做用域的上下文中執行,如此修改全局做用域。但不論那種狀況,eval(..)
均可以在運行時修改一個編寫時的詞法做用域。
注意: 當eval(..)
被用於一個操做它本身的詞法做用域的strict模式程序時,在eval(..)
內部作出的聲明不會實際上修改包圍它的做用域。
var b = 2;
function demo(str, a) {
'use strict'
eval(str); // 欺騙詞法做用域不生效
console.log(a, b);
}
demo("var b = 12;", 1); // 1, 2
複製代碼
在JavaScript中還有其餘的工具擁有與eval(..)
很是相似的效果。setTimeout(..)
和setInterval(..)
能夠 爲它們各自的第一個參數值接收一個字符串,其內容將會被eval
爲一個動態生成的函數的代碼。這種老舊的,遺產行爲早就被廢棄了。別這麼作!
new Function(..)
函數構造器相似地爲它的 最後 一個參數值接收一個代碼字符串,來把它轉換爲一個動態生成的函數(前面的參數值,若是有的話,將做爲新函數的命名參數)。這種函數構造器語法要比eval(..)
稍稍安全一些,但在你的代碼中它仍然應當被避免。
在你的代碼中動態生成代碼的用例少的難以想象,由於在性能上的倒退使得這種能力幾乎老是得不償失。
MDN最新規範不建議使用,因此接下來咱們瞭解下with語句就行。
with
語句接收一個對象,這個對象有0個或多個屬性,並 將這個對象視爲好像它是一個徹底隔離的詞法做用域,所以這個對象的屬性被視爲在這個「做用域」中詞法定義的標識符。
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2 -- 哦,全局做用域被泄漏了!
複製代碼
在這個代碼示例中,建立了兩個對象o1
和o2
。一個有a
屬性,而另外一個沒有。foo(..)
函數接收一個對象引用obj
做爲參數值,並在這個引用上調用with (obj) {..}
。在with
塊兒內部,咱們製造了一個變量a
的看似是普通詞法引用的東西,並將值2
賦予它。
當咱們傳入o1
時,賦值a = 2
找到屬性o1.a
並賦予它值2
,正如在後續的console.log(o1.a)
語句反應的那樣。然而,當咱們傳入o2
,由於它沒有a
屬性,沒有這樣的屬性被建立,因此o2.a
仍是undefined
。
可是以後咱們注意到一個特別的反作用,賦值a = 2
建立了一個全局變量a
。這怎麼可能?
注意: 儘管一個with
塊兒將一個對象視爲一個詞法做用域,可是在with
塊兒內部的一個普通var
聲明將不會歸於這個with
塊兒的做用域,而是歸於包含它的函數做用域。
with
語句其實是從你傳遞給它的對象中憑空製造了一個 全新的詞法做用域。
以這種方式理解的話,當咱們傳入o1
時with
語句聲明的「做用域」就是o1
,並且這個「做用域」擁有一個對應於o1.a
屬性的「標識符」。但當咱們使用o2
做爲「做用域」時,它裏面沒有這樣的a
「標識符」,因而就會出現undefined
「做用域」o2
中沒有,foo(..)
的做用域中也沒有,甚至連全局做做用域中都沒有找到標識符a
,因此當a = 2
被執行時,其結果就是自動全局變量被建立(由於咱們沒有在strict模式下)。
with
在運行時將一個對象和它的屬性轉換爲一個帶有「標識符」的「做用域」,這個奇怪想法有些燒腦。可是對於咱們看到的結果來講,這是我能給出的最清晰的解釋。
經過在運行時修改,或建立新的詞法做用域,eval(..)
和with
均可以欺騙編寫時定義的詞法做用域。
JavaScript 引擎 在編譯階段期行許多性能優化工做。其中的一些優化原理都歸結爲實質上在進行詞法分析時能夠靜態地分析代碼,並提早決定全部的變量和函數聲明都在什麼位置,這樣在執行期間就能夠少花些力氣來解析標識符。
但若是 引擎 在代碼中找到一個eval(..)
或with
,它實質上就不得不 假定 本身知道的全部的標識符的位置多是不合法的,由於它不可能在詞法分析時就知道你將會向eval(..)
傳遞什麼樣的代碼來修改詞法做用域,或者你可能會向with
傳遞的對象有什麼樣的內容來建立一個新的將被查詢的詞法做用域。
換句話說,悲觀地看,若是eval(..)
或with
出現,那麼它 將 作的幾乎全部的優化都會變得沒有意義,因此它就會簡單地根本不作任何優化。
你的代碼幾乎確定會趨於運行的更慢,只由於你在代碼的任何地方引入了一個了eval(..)
或with
。不管 引擎 將在努力限制這些悲觀臆測的反作用上表現得多麼聰明,都沒有任何辦法能夠繞過這個事實:沒有優化,代碼就運行的更慢。