You don't KnowJS
引語:你不懂的JS這本書?github上已經有了7w的star最近也是張野大大給我推薦了一波,閱讀過之后感覺對js的基礎又有了更好的理解。本來我是從來不這種讀書筆記的,但是這本書的內容實在是太多太多哪哪都是重點。所以
也就決定記錄以下重要的地方便于以后復習方便如果有錯誤,感謝指出
第一部分:作用域和閉包
第一章
編譯的三個步驟(當然也就是編譯器干的事情了)
- 分詞/詞法分析?
通俗來說就是?編譯器會將我們寫的代碼首先拆分成可以進行編譯的代碼 eg:var a = 2;可以被編譯器分割為var,a,=,2,; 空格是否會被當作詞法單元,取決于空格在這門語言中是否具有意義。 - 解析/語法分析
AST:抽象語法樹的概念他會把上述分割好的代碼進組裝成為一個語法樹?m,=,var,a,2 都回變成語法樹的各個節點,從而為編譯做準備。 - 代碼生成
編譯器最終會將這樣的AST語法樹編譯為可執行的底層代碼。特別要強調的是JS的引擎在編譯器執行?是會幫助編譯器做代碼優化,同通常來說他不會編譯的過程就發生在引擎執行代碼的前很短的時間,并不是像執行?C/C++等這些代碼需要先build完整個文件再進行run這樣的方式。
理解作用域 (通常指的是詞法作用域或者也可以叫做靜態作用域)
首先說一下基本的執行順序首先是編譯器由上面的步驟編譯代碼然后對于一些變量的聲明會在編譯期間交給作用域然后作用域就會組成一個
像是一個樹的結構,全局作用域下面會有嵌套的函數作用域。最后JS引擎根據作用域去執行代碼,大概就是這樣的一個流程。
介紹以下三個關鍵的概念:
- 編譯器: 用來在引擎執行代碼前提供給引擎代碼并且向作用域提供組成“樹”的節點
- 引擎:用來負責執行和編譯的環境 配合作用域組成自己的上下文
- 作用域:負責收集并維護由所有聲明的標識符(變量)組成的一系列查詢,并實施一套非常嚴格的規則,確定當前執行的代碼對這些標識符的訪問權限。
LHS和RHS:“賦值操作的目標是誰(LHS)”以及“誰是賦值操作的源頭(RHS)”。PS:rhs參考對象為語句中的常量例如:console.log(1)就是誰來對于1進行操作,lh參考對象為語句中的常量例如:a = 22應該賦值給誰如果是 console.log(a)應該就是RHS和LHS一起
引擎和作用域的關系
下面是我寫的書上的題
測驗的答案:LHS:foo->c,2->a,a->b
ps:1. 可以理解為foo需要知道自己應該賦值給誰所以LHR
ps:2. 可以理解為2需要知道自己賦值給誰這里是foo的參數a
ps:3. 可以理解為a需要給誰賦值
測試的答案:RHS:2->foo,a->foo,b->a,a+b->return
ps: 1. 可以理解為是誰調用2,所以是foo
ps:2. 同上可以知道后續的三個答案
作用域的嵌套
作用域的嵌套:作用域是個家族,爸爸認識兒子的人,爺爺認識爸爸認識的人,每次問兒子有沒有有認識的人,如果沒有再問爸爸。 也就是
上文提到的樹結構
ReferenceError:你找了作用域整個家族都不認識的人就會出錯,并沒有申明這個引用了沒有聲明的變量的錯誤.
LHS查詢的時候需要特別注意的是 如果LHS在全局作用域當中都無法找到變量就會創建一個變量(非嚴格模式)
如果查找的目的是對變量進行賦值,那么就會使用 LHS 查詢;如果目的是獲取變量的值,就會使用 RHS 查詢
第二章: 詞法的作用域
詞法階段
1.一個詞法不可能同時在兩個作用域中,作用域查找會在找到第一個匹配的標識符時停止
2.全局變量會自動成為全局對象的屬性(據阮老師的博客上說這是由于js的設計這為了減少內存了留下的歷史問題)
3.無論函數在哪里被調用,也無論它如何被調用,它的詞法作用域都只由函數被聲明時所處的位置決定。(詞法作用域是靜態作用域和動態的沒有關系)
欺騙詞法作用域
1.eval函數:接受字符串代碼他會在編譯器執行在引擎快要執行的時候將這段代碼寫在他位于的位置,不推薦使用.不過可以解決var出現的變量死區的問題
2.with函數:簡單來說with函數{}以內的語句在當前的位置以內創建了一個作用域而且自動放入了吧obj對象當中的屬性放了進去,這就有點想是在Chrome中的命令行寫global.a = 0然后a=1進行賦值時一樣的,依然不推薦使用
//with的用法
var obj = {
a: 1,
b: 2,
c: 3
};
// 單調乏味的重復 "obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 簡單但是不快捷的方式
with (obj) {
a = 3;
b = 4;
c = 5;
}
//obj.a = 3
/***************我是分界線*****************/
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——不好,a 被泄漏到全局作用域上了!
這就是前面說的o2.a會進行LHS查詢當查詢到頂級時就會給全局變量賦值
eval和with我認為并不是作為詞法作用域的范圍,因為詞法作用域是在編譯前就做好的,所以這個叫做欺騙詞法作用域,當然因為是動態的所以會消耗性能
第三章:函數作用域和塊作用域
函數中的作用域
先看下面代碼函數bar(..) 擁有自己的作用域范圍,全局作用域也有自己的作用域范圍,它只包含了一個標識符:foo。而foo可以理解為一層樓進入一個房間的門,是一個入口.函數作用域?
主要提供函數變量的訪問,找不到一個變量就會去上一個作用域找,而之后所提到的原型鏈是一個對象的原型鏈是在這個對象的內部找屬性找不到的時候就會去查找.(自己在看書的時候不小心弄混了)
function foo(a) {
var b = 2;
// 一些代碼
function bar() {
// ...
}
var c = 3;
}
隱藏內部實現
在下面的代碼當中doSomethingElse的調用并不是最安全的,因為其他函數都可以調用
function doSomething(a) {
b = a + doSomethingElse( a * 2 );
console.log( b * 3 );
}
function doSomethingElse(a) {
return a - 1;
}
var b;
doSomething( 2 ); // 15
而下面的函數則是比較安全的
function doSomething(a) {
function doSomethingElse(a) {
return a - 1;
}
var b;
b = a + doSomethingElse( a * 2 );
console.log( b * 3 );
}
doSomething( 2 ); // 15
所以function(){}用來隱藏代碼解決沖突(這是因為js在es5當中只有函數作用域并沒有塊作用域)
函數作用域
js當中為了能夠模仿塊級作用域,邊有人想到了用函數作用域模仿的概念.先來看看下面代碼
//并不理想
function foo() {
var a = 3;
console.log( a );
}
foo();
//方法1:
(function foo(){
console.log( a ); // 3
})();
雖然這種技術可以解決一些問題,但是它并不理想,因為會導致一些額外的問題。首先,必須聲明一個具名函數 foo(),意味著 foo 這個名稱本身“污染”了所在作用域(在這個 例子中是全局作用域)。其次,必須顯式地通過函數名foo()調用這個函數才能運行其中的代碼。然而使用了自執行函數以后欺騙編譯器對于通過()或者+-*等等欺騙了編譯器的檢查(后面會提到)
所以忽略了function的聲明語句.而這個語句的結果值就是這個函數調用以后就會執行
匿名函數和立即執行函數還有函數的聲明和函數表達式
編寫帶有名字的函數便于理解
setTimeout( function timeoutHandler() {
// <-- 快看,我有名字了!
console.log( "I waited 1 second!" );
}, 1000 );
function 和 var 編譯時存在函數的提升,也就是說var 和 function會優先的被編譯器識別交給作用域。然后引擎在訪問代碼的時候就能夠查詢到編譯器交過來的變量了
下面說的方法其實是通過一些其他的表達式干擾編譯器的判斷,讓編譯器認為這并不是一個聲明,對于函數的表達式和函數的聲明還有立即執行函數可以看看這兩個博主的文章?看看我還有我
1.編譯器在編譯代碼的時候發現一行代碼如果第一個出現的是funtion則會被理解為函數的聲明語句(var也是而let的機制可能就不同),編譯器就會自動把它叫給作用域.函數的聲明是不會有結果值的
2.當一個有function的函數的聲明加入其他的東西時(例如括號+或者-等)編譯器會把他認為是一個非聲明的語句,而這些語句是需要引擎來執行的
3.當引擎執行代碼的時候會發現這里面藏著一個非聲明的語句于是就執行他這時候是有結果值的,所以可以對他進行調用
下面的代碼就是例子(感受一下js的黑暗吧)
//下面對可以對返回的結果值進行調用,括號的位置并不影響因為function(){}被作為表達式執行完以后會就會返回函數所以兩個都行
( function foo(){} )()//? foo(){}
( function foo(){} () )//? foo(){}
//我自己又寫了一下感受邪惡吧
(function(){return (a)=>{ console.log(a); return (b)=>{console.log(b)}}})()(1)(2)
//下面沒有返回的結果值就不可以所以報錯
function foo(){}()
//所以你可以依靠this和作用域來實現let,如果沒有聲明就會出錯
function foo(){console.log(a); (function(){this.a = 10})(); console.log(a); }
function foo(){console.log(a); let a = 10; console.log(a); }
塊作用域
{}無法創建塊作用域因為js并不支持塊作用域,但是try{}catch{}卻可以,和function(){
}我認為他們創建的其實是函數作用域,其實他們一直是在用函數作用域模擬塊作用域
第四章:提升
函數優先的原則
看下面的代碼
foo(); // 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};
上面的代碼會執行1和下面的代碼是等價的,這說明函數的聲明是要比var提前的,我認為可能編譯器在發現有function生命的時候會把var替換掉
function foo() {
console.log( 1 );
}
foo(); // 1
foo = function() { console.log( 2 );
};
第五章: 閉包
什么是閉包
其實我把閉包想象為一個被保存的作用域,而實現方式通常使用function(){}創建這樣一個函數作用域的方式(當然也有其他的方式)
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,這就是閉包的效果。
看起來你并不覺得這有什么牛逼的地方,但是其實js當中閉包是十分常用的功能(比如所有的回調函數其實都是閉包)
//當你把閉包的返回進入另一個函數內部的時候,你就可以在另一個函數內部訪問他的變量!!!!!!!
function foo() {
var a=2;
function baz() {
console.log( a ); // 2
}
bar( baz );
}
function bar(fn) {
fn(); // 媽媽快看呀,這就是閉包!
}
//通過作用域訪問的方式進行傳遞閉包
var fn;
function foo() {
var a=2;
function baz() {
console.log( a );
}
fn = baz; // 將 baz 分配給全局變量
}
function bar() {
fn(); // 媽媽快看呀,這就是閉包!
}
foo();
bar(); // 2
作者也告訴我們不僅僅如此,閉包之所用重要是因為 在定時器、事件監聽器、 Ajax請求、跨窗口通信、Web Workers或者任何其他的異步(或者同步)任務中,只要使用了回調函數,本質都是在使用閉包!
function wait() {
let a = 1;
function test(){
console.log(this.a)
console.log(a)
}
return test;
}
var a = 2;
wait()();
// 定時器
function wait(message) {
//這就是閉包
function timer() {
console.log( message );
}
//下面的就可以理解為引擎會在1s內調用一個函數,而這個函數就是閉包,他會訪問message的作用域
setTimeout( timer, 1000 );
}
wait( "Hello, closure!" );
//?事件監聽器
function setupBot(name, selector) {
$( selector ).click( function activator() {
console.log( "Activating: " + name ); });
}
setupBot( "Closure Bot 1", "#bot_1" );
setupBot( "Closure Bot 2", "#bot_2" );
//觸發的activator函數也可以看做是一個閉包
循環和閉包
先看看代碼
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
其實以上的輸出結果并不會是1,2,3,4,5.反而回是6,6,6,6,6.這是因為setTimeout閉包并不是立即執行的,而是延遲執行的.所以第一步會先把for循環走完,當延遲執行的函數重新回到這個作用域的時候,這里的變量已經面目全非了,所以為了能夠維護閉包調用的作用域我們會才去一些措施(我記得大搜車的筆試題就有這個)
//這樣是不行的,雖然我們確實創建了一個供閉包將來回頭查看的作用域,但是這個作用域里面什么都沒有
for (var i=1; i<=5; i++) {
(function() {
setTimeout( function timer() { console.log( i );}, i*1000 );})();
}
//所以像下面這樣的才能夠運行,因為這里面維護的作用域就不再是空了,當然也是因為這里面是一個值變量
for (var i=1; i<=5; i++) {
(function() {
var j = i;
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})(); }
模塊
在前端方面最早的模塊機制的實現其實就是閉包,開頭我說通常使用function(){}來維持一個特定的作用域,而下面的返回object的對象將各個維持特定作用域的function(){}組合起來,也能夠實現閉包.
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
首先,CoolModule() 只是一個函數,必須要通過調用它來創建一個模塊實例。如果不執行外部函數,內部作用域和閉包都無法被創建。
其次,CoolModule()返回一個用對象字面量語法{ key: value, ... }來表示的對象。這個返回的對象中含有對內部函數而不是內部數據變量的引用。我們需要保持內部數據變量是隱藏且私有的狀態。可以將這個對象類型的返回值看作本質上是模塊的公共 API。
從模塊中返回一個實際的對象并不是必須的,也可以直接返回一個內部函數。jQuery 就是一個很好的例子。jQuery 和 $ 標識符就是 juery 模塊的公共 API但它們本身都是函數(使用jq的時候其實是調用了他的構造函數創造了一個jq的節點)
這樣?就實現了訪問API中的方法但是卻又不會使變量污染,但是你必須使用它然后自己賦值一個變量
閉包的形成必須有兩個條件:1.必須有像上面一CoolModule()一樣的封閉函數,也就是閉包所能保留的作用域范圍.2.封閉函數至少要返回一個函數去作為探測這個作用域的閉包
現代和未來的模塊機制
看看下面代碼
//這里的模塊定義就像上面的那樣返回的是由閉包函數組成的對象
var MyModules = (
function Manager() {
var modules = {};
//這個函數是將創建定義,模塊的名字,和制定自己所依賴的模塊當你需要依賴其他模塊的時候就會在這里進行加載,傳入實現函數進行加載,最后是這個模塊的實現
function define(name, deps, impl) {
for (var i=0; i<deps.length; i++) {
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply( impl, deps );
}
function get(name) { return modules[name];}
return {
define: define,
get: get
}
})();
//首先定義一個自己的模塊bar,用來分裝一個說你好的方式
MyModules.define( "bar", [], function() {
function hello(who) {return "Let me introduce: " + who; }
return {
hello: hello
};});
//foo依賴于
MyModules.define( "foo", ["bar"], function(bar) {
var hungry = "hippo";
function awesome(){
console.log(bar.hello(hungry).toUpperCase())
}
return {
awesome: awesome
};
});
var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );
console.log(bar.hello( "hippo" )); // Let me introduce: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO
實際上在foo中得到的閉包bar閉包和 var bar = MyModules.get( "bar" );得到的閉包是一樣的
未來的模塊機制
- 在es6中會把一個文件當做一個模塊,我個人理解就是用一個(function 文件名(導入的其他文件){})將整個文件代碼括起來
- es6的模塊是比較穩定的,在之前的模塊機制用函數來分裝模塊?會導致只有在引擎?執行代碼的時候才會知道為什么錯,但是es6的模塊機制會被編譯器識別?也就會在執行知道?將會有什么錯誤.
- 這里對應的應該是require和import的區別因為在?webstorm編寫代碼的時候,require即便是路徑寫的不對也不會在webstorm出現錯誤,但是使用import導入的時候如果不存在webstorm會提示你沒辦法導入,我想著就是webstorm后臺為你編譯進行提示錯誤吧
- 還有一點好處是如果b塊里面用了c,當b導入到a中c也就會自己導入
- module和import的區別
import 可以將一個模塊中的一個或多個 API 導入到當前作用域中,并分別綁定在一個變量 上(在我們的例子里是 hello)。module 會將整個模塊的 API 導入并綁定到一個變量上(在 我們的例子里是 foo 和 bar)。export 會將當前模塊的一個標識符(變量、函數)導出為公 共 API。這些操作可以在模塊定義中根據需要使用任意多次。
附錄的內容
動態作用域
通過前面的學習我們知道靜態作用域也就是詞法作用域,也就是詞法作用域是由編譯器提前執行代碼的時候構造出來的一個作用域,我覺得他一定是采用樹進行存儲的.而動態的作用域實際上更多的是指的this指針,也就是說在引擎執行代碼的過程中進行變化的.(大部分的作用域應該是詞法作用域,但是難免的要使用一些在執行過程中變化的作用域)
function foo(){
console.log(a)//2
}
function bar(){
var a = 3;
foo();
}
var a = 2;
bar();
上面的代碼當中foo()作用域中沒有a變量,也就是說要執行RHS引用(當然也沒有console變量他會執行LHS引用)所以會找到2
塊作用域
之前說過js中是沒有塊級作用域的但是這其實并不是一個正常的編程語言的行為,所以模擬塊級作用域是非常重要的.其實在一些語法中就已經有了塊級作用域
比如with 和 catch
try{throw 2;}catch(a){
console.log( a ); // 2
}
console.log( a ); // ReferenceError
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的參數變量所以調用的時候其實是cool()這種方式。除了文章中提到的self保存住this的方法,就是使用箭頭函數的綁定可以寫成這個樣子
var obj = {
count: 0,
cool: function coolFn() {
if (this.count < 1) {
setTimeout( () => { // 箭頭函數是什么鬼東西?
this.count++;
console.log( "awesome?" );
}, 100 );
}
}
};
obj.cool(); // "awesome?"
箭頭函數的筆記在后面還會詳細的學習記錄下。
遺留問題
- 最后我還是沒有弄懂,附錄中動態作用域的問題,作者也說了動態作用域關心的是這個調用的位置而不是聲明的位置,所以如果按照作者的動態作用域的觀點會輸出this可是
作者自己又否定了說this的實現原理并不是一個純粹的動態作用域。那他到底是個什么?
function foo(){
console.log(a)//2
}
function bar(){
var a = 3;
foo();
}
var a = 2;
bar();