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