摘要:作用域有兩種主要工作模型詞法作用域和動(dòng)態(tài)作用域。可能會(huì)有一些同學(xué)認(rèn)為是,那就是沒有搞清楚詞法作用域的概念。在嚴(yán)格模式下,在運(yùn)行時(shí)有自己的詞法作用域,意味著其中的聲明無法修改所在的作用域。
1. 兩種作用域
“作用域”我們知道是一套規(guī)則,用來管理引擎如何在當(dāng)前作用域以及嵌套的子作用域中根據(jù)標(biāo)識(shí)符名稱進(jìn)行變量查找。
作用域有兩種主要工作模型:詞法作用域和動(dòng)態(tài)作用域。
大多數(shù)語言采用的都是詞法作用域,少數(shù)語言采用動(dòng)態(tài)作用域(例如 Bash 腳本),這里我們主要討論詞法作用域。
2. 詞法大部分標(biāo)準(zhǔn)語言編譯器的第一個(gè)工作階段叫作詞法化。
簡(jiǎn)單地說,詞法作用域是由你在寫代碼時(shí)將變量和函數(shù)(塊)作用域?qū)懺谀睦飦頉Q定的。當(dāng)然,也會(huì)有一些方法來動(dòng)態(tài)修改作用域,后邊我會(huì)介紹。
舉個(gè)例子:
var a = 2; function foo1 () { console.log(a); } function foo2 () { var a = 10; foo1(); } foo2();
這里輸出結(jié)果是多少呢?
注意,這里結(jié)果打印的是 2。
可能會(huì)有一些同學(xué)認(rèn)為是 10,那就是沒有搞清楚詞法作用域的概念。
前邊介紹了,詞法作用域只取決于代碼書寫時(shí)的位置,那么在這個(gè)例子中,函數(shù) foo1 定義時(shí)的位置決定了它的作用域,通過下圖理解:
foo1 和 foo2 都是分別定義在全局作用域中的函數(shù),它們是并列的,所以在 foo1 的作用域鏈中并不包含 foo2 的作用域,雖然在 foo2 中調(diào)用了 foo1,但是 foo1 對(duì)變量 a 進(jìn)行 RHS 查詢時(shí),在自己的作用域沒有找到,引擎會(huì)去 foo1 的上級(jí)作用域(也就是全局作用域)中查找,而并不會(huì)去 foo2 的作用域中查找,最終在全局作用域中找到 a 的值為 2。
總結(jié)來說,無論函數(shù)在哪里被調(diào)用,也無論它如何被調(diào)用,它的詞法作用域都只由函數(shù)被聲明時(shí)所處的位置決定。
3. 欺騙詞法JavaScript 中有 3 種方式可以用來“欺騙詞法”,動(dòng)態(tài)改變作用域。
第一種: eval
JavaScript 中 eval(...) 函數(shù)可以接受一個(gè)字符串作為參數(shù),并將其中的內(nèi)容視為好像在書寫時(shí)就存在于程序中這個(gè)位置的代碼。
在執(zhí)行 eval(...) 之后的代碼時(shí),引擎并不知道或在意前面的代碼是以動(dòng)態(tài)形式插入進(jìn)來并對(duì)詞法作用域環(huán)境進(jìn)行修改的,引擎只會(huì)像往常一樣正常進(jìn)行詞法作用域的查找。
舉個(gè)例子:
function foo (str) { eval(str); // "欺騙"詞法 console.log(a); } var a = 2; foo("var a = 10;");
如大家所想,輸出結(jié)果為 10。
因?yàn)?eval("var a = 10;") 在 foo 的作用域中新創(chuàng)建了一個(gè)同名變量 a,引擎在 foo 作用域中對(duì) a 進(jìn)行 RHS 查詢,找到了新定義的 a,值為 10,所以不再向上查找全局作用域中的 a,所以導(dǎo)致輸出結(jié)果為 10,這就是 eval(...) 的作用。
在嚴(yán)格模式下,eval(...) 在運(yùn)行時(shí)有自己的詞法作用域,意味著其中的聲明無法修改所在的作用域。
"use strict;" function foo (str) { eval(str); // eval() 有自己的作用域,所以并不會(huì)修改 foo 的詞法作用域 console.log(a); } var a = 2; foo("var a = 10;");
這里輸出結(jié)果為 2。
JavaScript 中還有一些功能和 eval(...) 類似的函數(shù),例如 setTimeout(...) 和 setInterval(...) 的第一個(gè)參數(shù)可以是一個(gè)字符串,字符串的內(nèi)容可以解釋為一段動(dòng)態(tài)生成的代碼。這些功能已經(jīng)過時(shí)并且不被提倡,最好不要使用它們。new Function(...) 函數(shù)的最后一個(gè)參數(shù)也可以接受代碼字符串,并將其轉(zhuǎn)化為動(dòng)態(tài)生成的函數(shù),也盡量避免使用。
在程序中動(dòng)態(tài)生成代碼的使用場(chǎng)景非常罕見,因?yàn)樗鶐淼暮锰師o法抵消性能上的損失。
第二種: with
with 通常被當(dāng)做重復(fù)引用同一個(gè)對(duì)象中的多個(gè)屬性的快捷方式,可以不需要重復(fù)引用對(duì)象本身。
舉個(gè)例子:
var obj = { a: 2, b: 3 }; with (obj) { console.log(a); // 2 console.log(b); // 3 c = 4; }; console.log(c); // 4, c 被泄露到全局作用域上
如上所示,我們對(duì) c 進(jìn)行 LHS 查詢,因?yàn)樵?with 引入的新作用域中沒有找到 c,所以向上一級(jí)作用域(這里是全局作用域)查找,也沒有找到,在非嚴(yán)格模式下,在全局對(duì)象中新建了一個(gè)屬性 c 并賦值為 4。
with 可以將一個(gè)沒有或有多個(gè)屬性的對(duì)象處理為一個(gè)完全隔離的詞法作用域,因此這個(gè)對(duì)象的屬性也會(huì)被處理為定義在這個(gè)作用域中的詞法標(biāo)識(shí)符。
盡管 with 塊可以將一個(gè)對(duì)象處理為詞法作用域,但是這個(gè)塊內(nèi)部正常的 var 聲明并不會(huì)限制在這個(gè)塊作用域中,而是被添加到 with 所處的函數(shù)作用域中。
嚴(yán)格模式下,with 被完全禁止使用。
"use strict"; var obj = { a: 2, b: 3 }; with (obj) { console.log(a); console.log(b); c = 4; }; console.log(c);
第三種: try...catch
try...catch 可以測(cè)試代碼中的錯(cuò)誤。try 部分包含需要運(yùn)行的代碼,而 catch 部分包含錯(cuò)誤發(fā)生時(shí)運(yùn)行的代碼。
舉個(gè)例子:
try { foo(); } catch (err) { console.log(err); var a = 2; // 打印出 "ReferenceError: foo is not defined at:2:4" } console.log(a); // 2
當(dāng) try 中的代碼出現(xiàn)錯(cuò)誤時(shí),就會(huì)進(jìn)入 catch 塊,此時(shí)會(huì)把異常對(duì)象添加到作用域鏈的最前端,類似于 with 一樣,catch 中定義的局部變量也都會(huì)添加到包含 try...catch 的函數(shù)作用域(或全局作用域)中。
4. 性能JavaScript 引擎會(huì)在編譯階段進(jìn)行數(shù)項(xiàng)性能優(yōu)化。其中有些優(yōu)化依賴于能夠根據(jù)代碼的詞法進(jìn)行靜態(tài)分析,并預(yù)先確定所有變量和函數(shù)定義的位置,才能在執(zhí)行過程中快速找到標(biāo)識(shí)符。
但如果引擎在代碼中發(fā)現(xiàn)了 eval(...)、with 和 try...catch ,它只能簡(jiǎn)單的假設(shè)關(guān)于標(biāo)識(shí)符位置的判斷都是無效的,因?yàn)闊o法在詞法分析階段明確知道 eval(...) 會(huì)接受到什么代碼,這些代碼會(huì)如何對(duì)作用域進(jìn)行修改,也無法知道傳遞給 with 用來創(chuàng)建新詞法作用域的對(duì)象的內(nèi)容到底是什么。
最悲觀的情況是如果出現(xiàn)了這些動(dòng)態(tài)添加作用域的代碼,所有的優(yōu)化可能都是無意義的,因此最簡(jiǎn)單的做法就是完全不進(jìn)行任何優(yōu)化。
如果代碼中大量使用 eval(...) 和 with,那么運(yùn)行起來一定會(huì)變得非常緩慢。
5. 結(jié)論很多時(shí)候我們對(duì)代碼的分析出錯(cuò),就是源于對(duì)詞法作用域的忽略,所以讓我們重新審視代碼,繼續(xù)努力!
歡迎關(guān)注我的公眾號(hào)文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/81599.html
摘要:權(quán)威指南第六版關(guān)于閉包的說明采用詞法作用域,也就是說函數(shù)的執(zhí)行依賴于變量的作用域,這個(gè)作用域是在函數(shù)定義時(shí)決定的,而不是函數(shù)調(diào)用時(shí)決定的。閉包這個(gè)術(shù)語的來源指函數(shù)變量可以被隱藏于作用域鏈之內(nèi),因此看起來是函數(shù)將變量包裹了起來。 最近打算換工作,所以參加了幾次面試(國內(nèi)比較知名的幾家互聯(lián)網(wǎng)公司)。在面試的過程中每當(dāng)被問起閉包,我都會(huì)說閉包是作用域的問題?令人驚訝的是幾乎無一例外的當(dāng)我提到...
摘要:內(nèi)部的稱為內(nèi)部函數(shù)或閉包函數(shù)。過度使用閉包會(huì)導(dǎo)致性能下降。,閉包函數(shù)分為定義時(shí),和運(yùn)行時(shí)。循環(huán)會(huì)先運(yùn)行完畢,此時(shí),閉包函數(shù)并沒有運(yùn)行。閉包只能取得外部函數(shù)中的最后一個(gè)值。事件綁定種的匿名函數(shù)也是閉包函數(shù)。而對(duì)象中的閉包函數(shù),指向。 閉包概念解釋: 閉包(也叫詞法閉包或者函數(shù)閉包)。 在一個(gè)函數(shù)parent內(nèi)聲明另一個(gè)函數(shù)child,形成了嵌套。函數(shù)child使用了函數(shù)parent的參數(shù)...
摘要:的抽象語法樹中可能如下圖所示代碼生成將轉(zhuǎn)換為可執(zhí)行代碼的過程被稱為代碼生成。如果是,編譯器會(huì)忽略該聲明,繼續(xù)進(jìn)行編譯,否則它會(huì)要求在當(dāng)前作用域的集合中聲明一個(gè)新的變量,并命名為。 在學(xué)習(xí) javascript 的過程中,我們第一步最應(yīng)該了解和掌握的就是作用域,與之相關(guān)還有程序是怎么編譯的,變量是怎么查找的,js 引擎是什么,引擎和作用域的關(guān)系又是什么,這些是 javascript 這門...
摘要:此時(shí),中定義的局部變量就被保存在內(nèi)存中。所以當(dāng)執(zhí)行的時(shí)候,其真正的作用域是運(yùn)行時(shí)的作用域運(yùn)行時(shí)作用域詞法作用域所以第一次調(diào)用時(shí),由于是,所以返回而第二次返回是。因此在使用閉包時(shí),需要非常注意內(nèi)存泄漏的問題。 說起閉包,相信寫前端的同學(xué)都知道,而且相信在實(shí)際的項(xiàng)目中或多或少都已經(jīng)用到了閉包。那到底什么才是閉包,閉包又是怎么產(chǎn)生的呢? 1. 什么是閉包在阮老師的文章中提到: 閉包就是能夠讀...
摘要:關(guān)于點(diǎn)擊進(jìn)入項(xiàng)目是我于開始的一個(gè)項(xiàng)目,每個(gè)工作日發(fā)布一道面試題。即使這個(gè)時(shí)間周期內(nèi),小明取得多次滿分。創(chuàng)建作用域鏈在執(zhí)行期上下文的創(chuàng)建階段,作用域鏈?zhǔn)窃谧兞繉?duì)象之后創(chuàng)建的。這種一層一層的關(guān)系,就是作用域鏈。 關(guān)于【Step-By-Step】 Step-By-Step (點(diǎn)擊進(jìn)入項(xiàng)目) 是我于 2019-05-20 開始的一個(gè)項(xiàng)目,每個(gè)工作日發(fā)布一道面試題。每個(gè)周末我會(huì)仔細(xì)閱讀大家的答...
閱讀 2901·2019-08-30 15:55
閱讀 2011·2019-08-30 14:02
閱讀 1249·2019-08-29 15:23
閱讀 1015·2019-08-29 11:27
閱讀 469·2019-08-26 11:43
閱讀 3197·2019-08-26 10:32
閱讀 1261·2019-08-23 14:41
閱讀 3308·2019-08-23 14:41