摘要:比如程序會被分解為解析語法分析將詞法單元流轉換成一個由元素逐級嵌套所組成的代表了程序語法接口的書,又稱抽象語法樹。代碼生成將抽象語法樹轉換為機器能夠識別的指令。
本文首發在我的個人博客:http://muyunyun.cn/
《你不知道的JavaScript》系列叢書給出了很多顛覆以往對JavaScript認知的點, 讀完上卷,受益匪淺,于是對其精華的知識點進行了梳理。
什么是作用域作用域是一套規則,用于確定在何處以及如何查找變量。
編譯原理JavaScript是一門編譯語言。在傳統編譯語言的流程中,程序中一段源代碼在執行之前會經歷三個步驟,統稱為“編譯”。
分詞/詞法分析
將字符串分解成有意義的代碼塊,代碼塊又稱詞法單元。比如程序var a = 2;會被分解為var、a、=、2、;
解析/語法分析
將詞法單元流轉換成一個由元素逐級嵌套所組成的代表了程序語法接口的書,又稱“抽象語法樹”。
代碼生成
將抽象語法樹轉換為機器能夠識別的指令。
作用域 分別與編譯器、引擎進行配合完成代碼的解析
引擎執行時會與作用域進行交流,確定RHS與LHS查找具體變量,如果查找不到會拋出異常。
編譯器負責語法分析以及生成代碼。
作用域負責收集并維護所有變量組成的一系列查詢,并確定當前執行的代碼對這些變量的訪問權限。
對于 var a = 2 這條語句,首先編譯器會將其分為兩部分,一部分是 var a,一部分是 a = 2。編譯器會在編譯期間執行 var a,然后到作用域中去查找 a 變量,如果 a 變量在作用域中還沒有聲明,那么就在作用域中聲明 a 變量,如果 a 變量已經存在,那就忽略 var a 語句。然后編譯器會為 a = 2 這條語句生成執行代碼,以供引擎執行該賦值操作。所以我們平時所提到的變量提升,無非就是利用這個先聲明后賦值的原理而已!
異常對于 var a = 10 這條賦值語句,實際上是為了查找變量 a, 并且將 10 這個數值賦予它,這就是 LHS 查詢。 對于 console.log(a) 這條語句,實際上是為了查找 a 的值并將其打印出來,這是 RHS 查詢。
為什么區分 LHS 和 RHS 是一件重要的事情?
在非嚴格模式下,LHS 調用查找不到變量時會創建一個全局變量,RHS 查找不到變量時會拋出 ReferenceError。 在嚴格模式下,LHS 和 RHS 查找不到變量時都會拋出 ReferenceError。
作用域共有兩種主要的工作模型。第一種是最為普遍的,被大多數編程語言所采用的詞法作用域( JavaScript 中的作用域就是詞法作用域)。另外一種是動態作用域,仍有一些編程語言在使用(比如Bash腳本、Perl中的一些模式等)。
詞法作用域詞法作用域是一套關于引擎如何尋找變量以及會在何處找到變量的規則。詞法作用域最重要的特征是它的定義過程發生在代碼的書寫階段(假設沒有使用 eval() 或 with )。來看示例代碼:
function foo() { console.log(a); // 2 } function bar() { var a = 3; foo(); } var a = 2; bar()
詞法作用域讓foo()中的a通過RHS引用到了全局作用域中的a,因此會輸出2。
動態作用域而動態作用域只關心它們從何處調用。換句話說,作用域鏈是基于調用棧的,而不是代碼中的作用域嵌套。因此,如果 JavaScript 具有動態作用域,理論上,下面代碼中的 foo() 在執行時將會輸出3。
function foo() { console.log(a); // 3 } function bar() { var a = 3; foo(); } var a = 2; bar()函數作用域 匿名與具名
對于函數表達式一個最熟悉的場景可能就是回調函數了,比如
setTimeout( function() { console.log("I waited 1 second!") }, 1000 )
這叫作匿名函數表達式。函數表達式可以匿名,而函數聲明則不可以省略函數名。匿名函數表達式書寫起來簡單快捷,很多庫和工具也傾向鼓勵使用這種風格的代碼。但它也有幾個缺點需要考慮。
匿名函數在棧追蹤中不會顯示出有意義的函數名,使得調試很困難。
如果沒有函數名,當函數需要引用自身時只能使用已經過期的 arguments.callee 引用,比如在遞歸中。另一個函數需要引用自身的例子,是在事件觸發后事件監聽器需要解綁自身。
匿名函數省略了對于代碼可讀性 / 可理解性很重要的函數名。一個描述性的名稱可以讓代碼不言自明。
始終給函數表達式命名是一個最佳實踐:
setTimeout( function timeoutHandler() { // 我有名字了 console.log("I waited 1 second!") }, 1000 )提升 先有聲明還是先有賦值
考慮以下代碼:
a = 2; var a; console.log(a); // 2
考慮另外一段代碼
console.log(a); // undefined var a = 2;
我們習慣將 var a = 2; 看作一個聲明,而實際上 JavaScript 引擎并不這么認為。它將 var a 和 a = 2 當作兩個多帶帶的聲明,第一個是編譯階段的任務,而第二個是執行階段的任務。
這意味著無論作用域中的聲明出現在什么地方,都將在代碼本身被執行前首先進行處理。可以將這個過程形象地想象成所有的聲明(變量和函數)都會被“移動”到各自作用域的最頂端,這個過程稱為提升。
可以看出,先有聲明后有賦值。
再來看以下代碼:
foo(); // TypeError bar(); // ReferenceError var foo = function bar() { // ... };
這個代碼片段經過提升后,實際上會被理解為以下形式:
var foo; foo(); // TypeError bar(); // ReferenceError foo = function() { var bar = ...self... // ... };
這段程序中的變量標識符 foo() 被提升并分配給全局作用域,因此 foo() 不會導致 ReferenceError。但是 foo 此時并沒有賦值(如果它是一個函數聲明而不是函數表達式就會賦值)。foo()由于對 undefined 值進行函數調用而導致非法操作,因此拋出 TypeError 異常。另外即時是具名的函數表達式,名稱標識符(這里是 bar )在賦值之前也無法在所在作用域中使用。
閉包之前寫過關于閉包的一篇文章深入淺出JavaScript之閉包(Closure)
循環和閉包要說明閉包,for 循環是最常見的例子。
for (var i = 1; i <= 5; i++) { setTimeout( function timer() { console.log(i); }, i*1000 ) }
正常情況下,我們對這段代碼行為的預期是分別輸出數字 1~5,每秒一次,每次一個。但實際上,這段代碼在運行時會以每秒一次的頻率輸出五次6。
它的缺陷在于:根據作用域的工作原理,盡管循環中的五個函數是在各個迭代中分別定義的,但是它們都被封閉在一個共享的全局作用域中,因此實際上只有一個i。因此我們需要更多的閉包作用域。我們知道IIFE會通過聲明并立即執行一個函數來創建作用域,我們來進行改進:
for (var i = 1; i <= 5; i++) { (function() { var j = i; setTimeout( function timer() { console.log(j); }, j*1000 ) })(); }
還可以對這段代碼進行一些改進:
for (var i = 1; i <= 5; i++) { (function(j) { setTimeout( function timer() { console.log(j); }, j*1000 ) })(i); }
在迭代內使用 IIFE 會為每個迭代都生成一個新的作用域,使得延遲函數的回調可以將新的作用域封閉在每個迭代內部,每個迭代中都會含有一個具有正確值的變量供我們訪問。
重返塊作用域我們使用 IIFE 在每次迭代時都創建一個新的作用域。換句話說,每次迭代我們都需要一個塊作用域。我們知道 let 聲明可以用來劫持塊作用域,那我們可以進行這樣改:
for (var i = 1; i <= 5; i++) { let j = i; setTimeout( function timer() { console.log(j); }, j*1000 ) }
本質上這是將一個塊轉換成一個可以被關閉的作用域。
此外,for循環頭部的 let 聲明還會有一個特殊行為。這個行為指出每個迭代都會使用上一個迭代結束時的值來初始化這個變量。
for (let i = 1; i <= 5; i++) { setTimeout( function timer() { console.log(i); }, i*1000 ) }this全面解析
之前寫過一篇深入淺出JavaScript之this。我們知道this是在運行時進行綁定的,并不是在編寫時綁定,它的上下文取決于函數調用時的各種條件。this的綁定和函數聲明的位置沒有任何關系,只取決于函數的調用方式。
this詞法來看下面這段代碼的問題:
var obj = { id: "awesome", cool: function coolFn() { console.log(this.id); } }; var id = "not awesome"; obj.cool(); // awesome setTimeout( obj.cool, 100); // not awesome
obj.cool() 與 setTimeout( obj.cool, 100 ) 輸出結果不一樣的原因在于 cool() 函數丟失了同 this 之間的綁定。解決方法最常用的是 var self = this;
var obj = { count: 0, cool: function coolFn() { var self = this; if (self.count < 1) { setTimeout( function timer(){ self.count++; console.log("awesome?"); }, 100) } } } obj.cool(); // awesome?
這里用到的知識點是我們非常熟悉的詞法作用域。self 只是一個可以通過詞法作用域和閉包進行引用的標識符,不關心 this 綁定的過程中發生了什么。
ES6 中的箭頭函數引人了一個叫作 this 詞法的行為:
var obj = { count: 0, cool: function coolFn() { if (this.count < 1) { setTimeout( () => { this.count++; console.log("awesome?"); }, 100) } } } obj.cool(); // awesome?
箭頭函數棄用了所有普通 this 綁定規則,取而代之的是用當前的詞法作用域覆蓋了 this 本來的值。因此,這個代碼片段中的箭頭函數只是"繼承"了 cool() 函數的 this 綁定。
但是箭頭函數的缺點就是因為其是匿名的,上文已介紹過具名函數比匿名函數更可取的原因。而且箭頭函數將程序員們經常犯的一個錯誤給標準化了:混淆了 this 綁定規則和詞法作用域規則。
箭頭函數不僅僅意味著可以少寫代碼。本書的作者認為使用 bind() 是更靠得住的方式。
var obj = { count: 0, cool: function coolFn() { if (this.count < 1) { setTimeout( () => { this.count++; console.log("more awesome"); }.bind( this ), 100) } } } obj.cool(); // more awesome綁定規則
函數在執行的過程中,可以根據下面這4條綁定規則來判斷 this 綁定到哪。
默認綁定
獨立函數調用
隱式綁定
當函數引用有上下文對象時,隱式綁定規則會把函數調用中的 this 綁定到這個上下文對象
顯示綁定
call/apply
bind(本質是對call/apply函數的封裝 fn.apply( obj, arguments ))
第三方庫的許多函數都提供了一個可選的參數(上下文),其作用和 bind() 一樣,確保回調函數使用指定的 this
new 綁定
JavaScript 中的 new 機制實際上和面向類的語言完全不同
實際上并不存在所謂的“構造函數”,只有對于函數的“構造調用”
書中對4條綁定規則的優先級進行了驗證,得出以下的順序優先級:
函數是否在 new 中調用(new 綁定)?如果是的話 this 綁定的是新創建的對象。
函數是否通過 call、apply(顯式綁定)或者硬綁定(bind)調用?如果是的話,this 綁定的是指定對象。
函數是否在某個上下文對象中調用(隱式綁定)?如果是的話,this 綁定的是那個上下文對象。
如果都不是的話,使用默認綁定。在嚴格模式下,綁定到 undefined,否則綁定到全局對象。
被忽略的 this如果你把 null 或者 undefined 作為 this 的綁定對象傳入 call、apply 或者 bind,這些值在調用時會被忽略,實際應用的是默認規則。
什么時候會傳入 null/undefined 呢?一種非常常見的做法是用 apply(..) 來“展開”一個數組,并當作參數傳入一個函數。類似地,bind(..) 可以對參數進行柯里化(預先設置一些參數),如下代碼:
function foo(a, b) { console.log( "a:" + a + ", b:" + b ); } // 把數組"展開"成參數 foo.apply(null, [2, 3]); // a:2, b:3 // 使用 bind(..) 進行柯里化 var bar = foo.bind( null, 2); bar(3); // a:2, b:3
其中 ES6 中,可以用 ... 操作符代替 apply(..) 來“展開”數組,但是 ES6 中沒有柯里化的相關語法,因此還是需要使用 bind(..)。
使用 null 來忽略 this 綁定可能產生一些副作用。如果某個函數(比如第三庫中的某個函數)確實使用了 this ,默認綁定規則會把 this 綁定到全局對象,這將導致不可預計的后果。更安全的做法是傳入一個特殊的對象,一個 “DMZ” 對象,一個空的非委托對象,即 Object.create(null)。
function foo(a, b) { console.log( "a:" + a + ", b:" + b ); } var ? = Object.create(null); // 把數組"展開"成參數 foo.apply( ?, [2, 3]); // a:2, b:3 // 使用 bind(..) 進行柯里化 var bar = foo.bind( ?, 2); bar(3); // a:2, b:3對象
JavaScript中的對象有字面形式(比如var a = { .. })和構造形式(比如var a = new Array(..))。字面形式更常用,不過有時候構造形式可以提供更多選擇。
作者認為“JavaScript中萬物都是對象”的觀點是不對的。因為對象只是 6 個基礎類型( string、number、boolean、null、undefined、object )之一。對象有包括 function 在內的子對象,不同子類型具有不同的行為,比如內部標簽 [object Array] 表示這是對象的子類型數組。
復制對象思考一下這個對象:
function anotherFunction() { /*..*/ } var anotherObject = { c: true }; var anotherArray = []; var myObject = { a: 2, b: anotherObject, // 引用,不是復本! c: anotherArray, // 另一個引用! d: anotherFunction }; anotherArray.push( myObject )
如何準確地表示 myObject 的復制呢?
這里有一個知識點。
淺復制。復制出的新對象中 a 的值會復制舊對象中 a 的值,也就是 2,但是新對象中 b、c、d 三個屬性其實只是三個引用。
深復制。除了復制 myObject 以外還會復制 anotherArray。這時問題就來了,anotherArray 引用了 myObject, 所以又需要復制 myObject,這樣就會由于循環引用導致死循環。
對于 JSON 安全的對象(就是能用 JSON.stringify 序列號的字符串)來說,有一種巧妙的復制方法:
var newObj = JSON.parse( JSON.stringify(someObj) )
我認為這種方法就是深復制。相比于深復制,淺復制非常易懂并且問題要少得多,ES6 定義了 Object.assign(..) 方法來實現淺復制。 Object.assign(..) 方法的第一個參數是目標對象,之后還可以跟一個或多個源對象。它會遍歷一個或多個源對象的所有可枚舉的自由鍵并把它們復制到目標對象,最后返回目標對象,就像這樣:
var newObj = Object.assign( {}, myObject ); newObj.a; // 2 newObj.b === anotherObject; // true newObj.c === anotherArray; // true newObj.d === anotherFunction; // true類
JavaScript 有一些近似類的語法元素(比如 new 和 instanceof), 后來的 ES6 中新增了一些如 class 的關鍵字。但是 JavaScript 實際上并沒有類。類是一種設計模式,JavaScript 的機制其實和類完全不同。
類的繼承(委托)其實就是復制,但和其他語言中類的表現不同(其他語言類表現出來的都是復制行為),JavaScript 中的多態(在繼承鏈中不同層次名稱相同,但是功能不同的函數)并不表示子類和父類有關聯,子類得到的只是父類的一份復本。
JavaScript 通過顯示混入和隱式混入 call() 來模擬其他語言類的表現。此外,顯示混入實際上無法完全模擬類的復制行為,因為對象(和函數!別忘了函數也是對象)只能復制引用,無法復制被引用的對象或者函數本身。
檢查“類”關系思考下面的代碼:
function Foo() { // ... } Foo.prototype.blah = ...; var a = new Foo();
我們如何找出 a 的“祖先”(委托關系)呢?
方法一:a instanceof Foo; // true (對象 instanceof 函數)
方法二: Foo.prototype.isPrototypeOf(a); // true (對象 isPrototypeOf 對象)
方法三: Object.getPrototypeOf(a) === Foo.prototype; // true (Object.getPrototypeOf() 可以獲取一個對象的 [[Prototype]]) 鏈;
方法四: a.__proto__ == Foo.prototype; // true
構造函數函數不是構造函數,而是當且僅當使用 new 時,函數調用會變成“構造函數調用”。
使用 new 會在 prototype 生成一個 constructor 屬性,指向構造調用的函數。
constructor 并不表示被構造,而且 constructor 屬性并不是一個不可變屬性,它是不可枚舉的,但它是可以被修改的。
對象關聯來看下面的代碼:
var foo = { something: function() { console.log("Tell me something good..."); } }; var bar = Object.create(foo); bar.something(); // Tell me something good...
Object.create(..)會創建一個新對象 (bar) 并把它關聯到我們指定的對象 (foo),這樣我們就可以充分發揮 [[Prototype]] 機制的為例(委托)并且避免不必要的麻煩 (比如使用 new 的構造函數調用會生成 .prototype 和 .constructor 引用)。
Object.create(null) 會創建一個擁有空鏈接的對象,這個對象無法進行委托。由于這個對象沒有原型鏈,所以 instanceof 操作符無法進行判斷,因此總是會返回 false 。這些特殊的空對象通常被稱作“字典”,它們完全不會受到原型鏈的干擾,因此非常適合用來存儲數據。
我們并不需要類來創建兩個對象之間的關系,只需要通過委托來關聯對象就足夠了。而Object.create(..)不包含任何“類的詭計”,所以它可以完美地創建我們想要的關聯關系。
此書的第二章第6部分就把面對類和繼承和行為委托兩種設計模式進行了對比,我們可以看到行為委托是一種更加簡潔的設計模式,在這種設計模式中能感受到Object.create()的強大。
ES6中的Class來看一段 ES6中Class 的例子
class Widget { constructor(width, height) { this.width = width || 50; this.height = height || 50; this.$elem = null; } render($where){ if (this.$elem) { this.$elem.css({ width: this.width + "px", height: this.height + "px" }).appendTo($where); } } } class Button extends Widget { constructor(width, height, label) { super(width, height); this.label = label || "Default"; this.$elem = $("
除了語法更好看之外,ES6還有以下優點
基本上不再引用雜亂的 .prototype 了。
Button 聲明時直接 “繼承” 了 Widget。
可以通過 super(..)來實現相對多態,這樣任何方法都可以引用原型鏈上層的同名方法。
class 字面語法不能聲明屬性(只能聲明方法)。這是一種限制,但是它會排除掉許多不好的情況。
可以通過 extends 很自然地擴展對象(子)類型。
但是 class 就是完美的嗎?在傳統面向類的語言中,類定義之后就不會進行修改,所以類的設計模式就不支持修改。但JavaScript 最強大的特性之一就是它的動態性,在使用 class 的有些時候還是會用到 .prototype 以及碰到 super (期望動態綁定然而靜態綁定) 的問題,class 基本上都沒有提供解決方案。
這也是本書作者希望我們思考的問題。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/83370.html
摘要:本書屬于基礎類書籍,會有比較多的基礎知識,所以這里僅記錄平常不怎么容易注意到的知識點,不會全記,供大家和自己翻閱不錯,下冊的知識點就這么少,非常不推介看下冊上中下三本的讀書筆記你不知道的上讀書筆記你不知道的中讀書筆記你不知道的下讀書筆記第三 本書屬于基礎類書籍,會有比較多的基礎知識,所以這里僅記錄平常不怎么容易注意到的知識點,不會全記,供大家和自己翻閱; 不錯,下冊的知識點就這么少,非...
摘要:的分句會創建一個塊作用域,其聲明的變量僅在中有效。而閉包的神奇作用是阻止此事發生。依然持有對該作用域的引用,而這個引用就叫做閉包。當然,無論使用何種方式對函數類型的值進行傳遞,當函數在別處被調用時都可以觀察到閉包。 date: 16.12.8 Thursday 第一章 作用域是什么 LHS:賦值操作的目標是誰? 比如: a = 2; RHS:誰是賦值操作的源頭? 比如: conso...
摘要:但是如果非全局的變量如果被遮蔽了,無論如何都無法被訪問到。但是如果引擎在代碼中找到,就會完全不做任何優化。結構的分句中具有塊級作用域。第四章提升編譯器函數聲明會被提升,而函數表達式不會被提升。 本書屬于基礎類書籍,會有比較多的基礎知識,所以這里僅記錄平常不怎么容易注意到的知識點,不會全記,供大家和自己翻閱; 上中下三本的讀書筆記: 《你不知道的JavaScript》 (上) 讀書筆記...
摘要:有種內置類型,分別是除對象之外,其他統稱為基本類型。另一個需要注意的是數組確切地說,數組也是的一個子類型我們可以通過下面的方法檢查變量是不是數組處理未聲明的變量時,會返回這是因為有一個特殊的安全防范機制。 js有7種內置類型,分別是undefined null boolean string number symbol object除對象之 Object 外,其他統稱為基本類型。符號 ...
摘要:異步請求線程在在連接后是通過瀏覽器新開一個線程請求將檢測到狀態變更時,如果設置有回調函數,異步線程就產生狀態變更事件,將這個回調再放入事件循環隊列中。 基礎:瀏覽器 -- 多進程,每個tab頁獨立一個瀏覽器渲染進程(瀏覽器內核) 每個瀏覽器渲染進程是多線程的,主要包括:GUI渲染線程 JS引擎線程 也稱為JS內核,負責處理Javascript腳本程序。(例如V8引擎) JS引擎線程負...
閱讀 2741·2023-04-25 14:21
閱讀 1176·2021-11-23 09:51
閱讀 4019·2021-09-22 15:43
閱讀 612·2019-08-30 15:55
閱讀 1561·2019-08-29 11:28
閱讀 2448·2019-08-26 11:44
閱讀 1684·2019-08-23 18:15
閱讀 2883·2019-08-23 16:42