一、你不知道的JavaScript
1、作用域
作用域 LHS RHS
- RHS查詢與簡單地查找某個(gè)變量的值別無二致,而LHS查詢則是試圖找到變量的容器本身,從而可以對(duì)其賦值。從這個(gè)角度說,RHS并不是真正意義上的“賦值操作的右側(cè)”,更準(zhǔn)確地說是“非左側(cè)”。RHS理解成retrieve his source value(取到它的源值),這意味著“得到某某的值”。
- 非嚴(yán)格模式下,當(dāng)引擎執(zhí)行LHS查詢時(shí),如果在頂層(全局作用域)中也無法找到目標(biāo)變量,全局作用域中就會(huì)創(chuàng)建一個(gè)具有該名稱的變量,并將其返還給引擎。嚴(yán)格模式下則會(huì)拋出異常未聲明
ReferenceError,另外還有typeError -
提升: 引擎會(huì)在解釋JavaScript代碼之前首先對(duì)其進(jìn)行編譯。編譯階段中的一部分工作就是找到所有的聲明,并用合適的作用域?qū)⑺鼈冴P(guān)聯(lián)起來。當(dāng)你看到
var a = 2;時(shí),可能會(huì)認(rèn)為這是一個(gè)聲明。但JavaScript實(shí)際上會(huì)將其看成兩個(gè)聲明:var a;和a = 2;。第一個(gè)定義聲明是在編譯階段進(jìn)行的。第二個(gè)賦值聲明會(huì)被留在原地等待執(zhí)行階段。只有聲明本身會(huì)被提升,而賦值或其他運(yùn)行邏輯會(huì)留在原地。函數(shù)聲明的提升高于變量聲明。 - 匿名函數(shù)
(function(args){})(args) -
var和let,let關(guān)鍵字可以將變量綁定到所在的任意作用域中(通常是{ .. }內(nèi)部)。換句話說,let為其聲明的變量隱式地劫持了所在的塊作用域。let只能運(yùn)行在嚴(yán)格模式下。var foo = true; if (foo) { let bar = foo * 2; bar = something( bar ); console.log( bar ); } console.log( bar ); // 因?yàn)閘et , 對(duì)于用let聲明的bar,if(){}變成一個(gè)塊作用域,所以在外部引用bar 時(shí), 未聲明,不存在變量 ReferenceError var foo = true; if (foo) { var a = 2; const b = 3; // 包含在if中的塊作用域常量 a = 3; // 正常! b = 4; // 錯(cuò)誤! } console.log( a ); // 3 console.log( b ); // ReferenceError! //const同樣會(huì)劫持塊作用域,b在外部引用就是未聲明,var聲明的a則不存在劫持塊作用域,const定義變量值不可改變
2、閉包
-
把內(nèi)部函數(shù)baz傳遞給bar,當(dāng)調(diào)用這個(gè)內(nèi)部函數(shù)時(shí)(現(xiàn)在叫作fn),它涵蓋的foo()內(nèi)部作用域的閉包就可以觀察到了,因?yàn)樗軌蛟L問a。
function foo() { var a = 2; function baz() { console.log( a ); // 2 } bar( baz ); } function bar(fn) { fn(); // 媽媽快看呀,這就是閉包! } //間接傳遞函數(shù) var fn; function foo() { var a = 2; function baz() { console.log( a ); } fn = baz; // 將baz分配給全局變量 } function bar() { fn(); // 媽媽快看呀,這就是閉包! } foo(); bar(); // 2 // jquery example function setupBot(name, selector) { $( selector ).click( function activator() { console.log( "Activating:" + name ); } ); } setupBot( "Closure Bot 1", "#bot_1" ); setupBot( "Closure Bot 2", "#bot_2" ); -
雖然以下段代碼可以正常工作,但嚴(yán)格來講它并不是閉包。為什么?因?yàn)楹瘮?shù)(示例代碼中的IIFE)并不是在它本身的詞法作用域以外執(zhí)行的。它在定義時(shí)所在的作用域中執(zhí)行(而外部作用域,也就是全局作用域也持有a)。a是通過普通的詞法作用域查找而非閉包被發(fā)現(xiàn)的。
var a = 2; (function IIFE() { console.log( a ); })(); -
循環(huán)中的閉包
for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); } //運(yùn)行時(shí)會(huì)以每秒一次的頻率輸出五次6 // for循環(huán)每次迭代并沒有創(chuàng)建一個(gè)新的作用域 for (var i=1; i<=5; i++) { (function() { setTimeout( function timer() { console.log( i ); }, i*1000 ); })(); } // 不行,雖然每次迭代立即執(zhí)行函數(shù)創(chuàng)建新的作用域,但是一個(gè)空作用域,不包含i for (var i=1; i<=5; i++) { (function(j) { // 或則 var j = i; setTimeout( function timer() { console.log( j ); }, j*1000 ); })( i ); } // 每秒一次,輸出1~5 , 每次迭代,變量i當(dāng)前值賦予函數(shù)的參數(shù) j 并被封裝到函數(shù)作用域中。 // let劫持塊作用域,這樣for每次迭代對(duì)let聲明的i 都是一個(gè)新的塊作用域 for (let i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); } -
模塊,如下代碼:CoolModule()只是一個(gè)函數(shù),必須要通過調(diào)用它來創(chuàng)建一個(gè)模塊實(shí)例。如果不執(zhí)行外部函數(shù),內(nèi)部作用域和閉包都無法被創(chuàng)建。CoolModule()返回一個(gè)用對(duì)象字面量語法{ key: value, ... }來表示的對(duì)象。這個(gè)返回的對(duì)象中含有對(duì)內(nèi)部函數(shù)而不是內(nèi)部數(shù)據(jù)變量的引用。我們保持內(nèi)部數(shù)據(jù)變量是隱藏且私有的狀態(tài)??梢詫⑦@個(gè)對(duì)象類型的返回值看作本質(zhì)上是模塊的公共API。這個(gè)對(duì)象類型的返回值最終被賦值給外部的變量foo,然后就可以通過它來訪問API中的屬性方法,比如foo.doSomething()。
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 //我們將模塊函數(shù)轉(zhuǎn)換成了IIFE(參見第3章),立即調(diào)用這個(gè)函數(shù)并將返回值直接賦值給單例的模塊實(shí)例標(biāo)識(shí)符foo。 var foo = (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 }; })(); foo.doSomething(); // cool foo.doAnother(); // 1 ! 2 ! 3 //模塊也是普通的函數(shù),因此可以接受參數(shù) function CoolModule(id) { function identify() { console.log( id ); } return { identify: identify }; } var foo1 = CoolModule( "foo 1" ); var foo2 = CoolModule( "foo 2" ); foo1.identify(); // "foo 1" foo2.identify(); // "foo 2" //模塊模式另一個(gè)簡單但強(qiáng)大的用法是命名將要作為公共API返回的對(duì)象 var foo = (function CoolModule(id) { function change() { // 修改公共API publicAPI.identify = identify2; } function identify1() { console.log( id ); } function identify2() { console.log( id.toUpperCase() ); } var publicAPI = { change: change, identify: identify1 }; return publicAPI; })( "foo module" ); foo.identify(); // foo module foo.change(); foo.identify(); // FOO MODULE -
現(xiàn)代的模塊機(jī)制
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 ); // apply函數(shù) 繼承,deps作為參數(shù)傳入,此處為函數(shù)依賴的其他函數(shù) } function get(name) { return modules[name]; } return { define: define, get: get }; })(); //這段代碼的核心是modules[name] = impl.apply(impl, deps)。 //為了模塊的定義引入了包裝函數(shù)(可以傳入任何依賴),并且將返回值,也就是模塊的API,儲(chǔ)存在一個(gè)根據(jù)名字來管理的模塊列表中 MyModules.define( "bar", [], function() { function hello(who) { return "Let me introduce: " + who; } return { hello: hello }; } ); 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" ) ); // <i>Let me introduce: hippo</i> foo.awesome(); // LET ME INTRODUCE: HIPPO -
ES6模塊機(jī)制 , ES6的模塊沒有“行內(nèi)”格式,必須被定義在獨(dú)立的文件中(一個(gè)文件一個(gè)模塊)。瀏覽器或引擎有一個(gè)默認(rèn)的“模塊加載器”(可以被重載,但這遠(yuǎn)超出了我們的討論范圍)可以在導(dǎo)入模塊時(shí)同步地加載模塊文件。
//bar.js function hello(who) { return "Let me introduce: " + who; } export hello; //foo.js // 僅從"bar"模塊導(dǎo)入hello() import hello from "bar"; var hungry = "hippo"; function awesome() { console.log( hello( hungry ).toUpperCase() ); } export awesome;import可以將一個(gè)模塊中的一個(gè)或多個(gè)API導(dǎo)入到當(dāng)前作用域中,并分別綁定在一個(gè)變量上(在我
們的例子里是hello)。module會(huì)將整個(gè)模塊的API導(dǎo)入并綁定到一個(gè)變量上(在我們的例子里是
foo和bar)。export會(huì)將當(dāng)前模塊的一個(gè)標(biāo)識(shí)符(變量、函數(shù))導(dǎo)出為公共API。
當(dāng)函數(shù)可以記住并訪問所在的詞法作用域,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行,這時(shí)就產(chǎn)生了閉包。
3、 動(dòng)態(tài)作用域
- 事實(shí)上JavaScript并不具有動(dòng)態(tài)作用域。它只有詞法作用域,簡單明了。但是this機(jī)制某種程度上很像動(dòng)態(tài)作用域。
主要區(qū)別:詞法作用域是在寫代碼或者說定義時(shí)確定的,而動(dòng)態(tài)作用域是在運(yùn)行時(shí)確定的。(this也是?。┰~法作用域關(guān)注函數(shù)在何處聲明,而動(dòng)態(tài)作用域關(guān)注函數(shù)從何處調(diào)用function foo() { console.log( a ); // 2 } function bar() { var a = 3; foo(); } var a = 2; bar(); // foo中的a 會(huì)通過RHS查詢到全局的a,基于詞法作用域查找 // 詞法作用域最重要的特征是它的定義過程發(fā)生在代碼的書寫階段 // 如果基于動(dòng)態(tài)作用域,作用域鏈?zhǔn)腔谡{(diào)用棧的,即foo向上查找bar中的a
4、 this
對(duì)this的認(rèn)識(shí),this既不指向函數(shù)自身也不指向函數(shù)的詞法作用域,拋開以前錯(cuò)誤的假設(shè)和理解。this實(shí)際上是在函數(shù)被調(diào)用時(shí)發(fā)生的綁定,它指向什么完全取決于函數(shù)在哪里被調(diào)用。
1、 this并不像我們所想的那樣指向函數(shù)本身
2、this在任何情況下都不指向函數(shù)的詞法作用域。
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log( this.a );
}
foo(); // ReferenceError: a is not defined
this是在運(yùn)行時(shí)進(jìn)行綁定的,并不是在編寫時(shí)綁定,它的上下文取決于函數(shù)調(diào)用時(shí)的各種條件。this的綁定和函數(shù)聲明的位置沒有任何關(guān)系,只取決于函數(shù)的調(diào)用方式。
- 綁定規(guī)則
- 1、默認(rèn)綁定,當(dāng)調(diào)用foo()時(shí),this.a被解析成了全局變量a。為什么?因?yàn)樵诒纠?,函?shù)調(diào)用時(shí)應(yīng)用了this的默認(rèn)綁定,因此this指向全局對(duì)象。foo()是直接使用不帶任何修飾的函數(shù)引用進(jìn)行調(diào)用的,因此只能使用默認(rèn)綁定,無法應(yīng)用其他規(guī)則。如果使用嚴(yán)格模式(strict mode),則不能將全局對(duì)象用于默認(rèn)綁定,因此this會(huì)綁定到undefined
function foo() { // "use strict"; 嚴(yán)格模式 console.log( this.a ); } var a = 2; foo(); // 2 - 2、隱式綁定,如下,調(diào)用位置會(huì)使用obj上下文來引用函數(shù),因此你可以說函數(shù)被調(diào)用時(shí)obj對(duì)象“擁有”或者“包含”它。當(dāng)foo()被調(diào)用時(shí),它的前面確實(shí)加上了對(duì)obj的引用。當(dāng)函數(shù)引用有上下文對(duì)象時(shí),隱式綁定規(guī)則會(huì)把函數(shù)調(diào)用中的this綁定到這個(gè)上下文對(duì)象。因?yàn)檎{(diào)用foo()時(shí)this被綁定到obj,因此this.a和obj.a是一樣的。對(duì)象屬性引用鏈中只有上一層或者說最后一層在調(diào)用位置中起作用,如
obj1.obj2.foo();則foo()的this指向obj2。隱式綁定時(shí),我們必須在一個(gè)對(duì)象內(nèi)部包含一個(gè)指向函數(shù)的屬性,并通過這個(gè)屬性間接引用函數(shù),從而把this間接(隱式)綁定到這個(gè)對(duì)象上。
如下代碼,雖然bar是obj.foo的一個(gè)引用,但是實(shí)際上,它引用的是foo函數(shù)本身,因此此時(shí)的bar()其實(shí)是一個(gè)不帶任何修飾的函數(shù)調(diào)用,因此應(yīng)用了默認(rèn)綁定。function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; obj.foo(); // 2
參數(shù)傳遞其實(shí)就是一種隱式賦值,因此我們傳入函數(shù)時(shí)也會(huì)被隱式賦值,function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; var bar = obj.foo; // 函數(shù)別名! var a = "oops, global"; // a是全局對(duì)象的屬性 bar(); // "oops, globalfn = obj.foofunction foo() { console.log( this.a ); } function doFoo(fn) { // fn其實(shí)引用的是foo fn(); // <-- 調(diào)用位置! } var obj = { a: 2, foo: foo }; var a = "oops, global"; // a是全局對(duì)象的屬性 doFoo( obj.foo ); // "oops, global" //js的內(nèi)置函數(shù)一樣,將函數(shù)傳遞給另一函數(shù)的形參,其中包含了隱式賦值 function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; var a = "oops, global"; // a是全局對(duì)象的屬性 setTimeout( obj.foo, 100 ); // "oops, global" //JavaScript環(huán)境中內(nèi)置的setTimeout()函數(shù)實(shí)現(xiàn)和下面的偽代碼類似 function setTimeout(fn,delay) { // 等待delay毫秒 fn(); // <-- 調(diào)用位置! } -
顯示綁定,通過
call()和apply()函數(shù)實(shí)現(xiàn),它們的第一個(gè)參數(shù)是一個(gè)對(duì)象,是給this準(zhǔn)備的,接著在調(diào)用函數(shù)時(shí)將其綁定到this。因?yàn)槟憧梢灾苯又付╰his的綁定對(duì)象,因此我們稱之為顯式綁定。function foo() { console.log( this.a ); } var obj = { a:2 }; //顯示指定foo函數(shù)中的this并執(zhí)行該函數(shù) foo.call( obj ); // 2 //硬綁定,是一種非常常用的模式,所以ES5提供了內(nèi)置的方法Function.prototype.bind,它的用法如下 function foo(something) { console.log( this.a, something ); return this.a + something; } var obj = { a:2 }; var bar = foo.bind( obj );//將foo函數(shù)的this綁定到obj,返回一個(gè)新的foo函數(shù)實(shí)例bar var b = bar( 3 ); // 2 3 console.log( b ); // 5 //第三方庫的許多函數(shù),以及JavaScript語言和宿主環(huán)境中許多新的內(nèi)置函數(shù),都提供了一個(gè)可選的參數(shù), //通常被稱為“上下文”(context),其作用和bind(..)一樣,確保你的回調(diào)函數(shù)使用指定的this。 function foo(el) { console.log( el, this.id ); } var obj = { id: "awesome" }; // 調(diào)用foo(..)時(shí)把this綁定到obj [1, 2, 3].forEach( foo, obj ); // 1 awesome 2 awesome 3 awesome //這些函數(shù)實(shí)際上就是通過call(..)或者apply(..)實(shí)現(xiàn)了顯式綁定,這樣你可以少寫一些代碼。 -
new 綁定
function foo(a) { this.a = a; } var bar = new foo(2); //new foo(2)執(zhí)行時(shí) foo的this指向new新建的對(duì)象foo //若不用new直接執(zhí)行foo(),其this指向全局作用域,同默認(rèn)規(guī)則 console.log( bar.a ); // 2 // 使用new來調(diào)用函數(shù),或者說發(fā)生構(gòu)造函數(shù)調(diào)用時(shí),會(huì)自動(dòng)執(zhí)行下面的操作 //1. 創(chuàng)建(或者說構(gòu)造)一個(gè)全新的對(duì)象。 //2. 這個(gè)新對(duì)象會(huì)被執(zhí)行[[Prototype]]連接。 //3. 這個(gè)新對(duì)象會(huì)綁定到函數(shù)調(diào)用的this。 //4. 如果函數(shù)沒有返回其他對(duì)象,那么new表達(dá)式中的函數(shù)調(diào)用會(huì)自動(dòng)返回這個(gè)新對(duì)象。 //new foo(2)創(chuàng)建一個(gè)全新的foo對(duì)象,this指向的就是foo對(duì)象,返回foo
- 1、默認(rèn)綁定,當(dāng)調(diào)用foo()時(shí),this.a被解析成了全局變量a。為什么?因?yàn)樵诒纠?,函?shù)調(diào)用時(shí)應(yīng)用了this的默認(rèn)綁定,因此this指向全局對(duì)象。foo()是直接使用不帶任何修飾的函數(shù)引用進(jìn)行調(diào)用的,因此只能使用默認(rèn)綁定,無法應(yīng)用其他規(guī)則。如果使用嚴(yán)格模式(strict mode),則不能將全局對(duì)象用于默認(rèn)綁定,因此this會(huì)綁定到undefined
-
胖箭頭
=>,箭頭函數(shù)不使用this的四種標(biāo)準(zhǔn)規(guī)則,而是根據(jù)外層(函數(shù)或者全局)作用域來決定this。箭頭函數(shù)的綁定無法被修改。function foo() { setTimeout(() => { // 這里的this在詞法上繼承自foo() console.log( this.a ); },100); } var obj = { a:2 }; foo.call( obj ); // 2
5、對(duì)象
-
語法,聲明
var myObj = { key: value // ... }; var myObj = new Object(); myObj.key = value; -
對(duì)象類型
// 對(duì)象是JavaScript的基礎(chǔ)。在JavaScript中一共有六種主要類型,基本類型(術(shù)語是“語言類型”): string number boolean null undefined object //基本類型按照值傳遞 //內(nèi)置對(duì)象 String Number Boolean Object Function Array Date RegExp Error -
值傳遞,基本類型按照值傳遞,對(duì)象其實(shí)也是按值傳遞。js的基礎(chǔ)類型原始值存儲(chǔ)在棧中,對(duì)象存儲(chǔ)在堆中,堆地址不能直接訪問,所有棧中存儲(chǔ)它的地址,引用值是存儲(chǔ)棧中的指向堆的地址。參考https://www.zhihu.com/question/27114726/answer/35481766
var obj = {x : 1}; function foo(o) { o = 100; } foo(obj); console.log(obj.x); // 仍然是1, obj并未被修改為100. // var obj1 = { value:'111' }; var obj2 = { value:'222' }; function changeStuff(obj){ obj.value = '333'; obj = obj2; return obj.value; } var foo = changeStuff(obj1); console.log(foo);// '222' 參數(shù)obj指向了新的對(duì)象obj2 console.log(obj1.value);//'333' -
ES6增加了可計(jì)算屬性名,可以將對(duì)象的key用變量表示,即對(duì)變量加上
[]var prefix = "foo"; var a="bar" var myObject = { [a]: "hello", ["baz"]: "world" }; console.log(myObject["bar"]); // hello console.log(myObject["baz"]); // world -
ES6定義了Object.assign(..)方法來實(shí)現(xiàn)淺復(fù)制,方法的第一個(gè)參數(shù)是目標(biāo)對(duì)象,之后還可以跟一個(gè)或多個(gè)源對(duì)象。
var newObj = Object.assign( {}, myObject ); newObj.a; // 2 newObj.b === anotherObject; // true newObj.c === anotherArray; // true newObj.d === anotherFunction; // true
6、類
二、Node.js Interview
1、內(nèi)存釋放
- 引用類型是在沒有引用之后, 通過 v8 的 GC 自動(dòng)回收, 值類型如果是處于閉包的情況下, 要等閉包沒有引用才會(huì)被 GC 回收, 非閉包的情況下等待 v8 的新生代 (new space) 切換的時(shí)候回收.
-
內(nèi)存泄漏幾種情況
-
全局變量
a = 10; //未聲明對(duì)象。 global.b = 11; //全局變量引用 //全局變量直接掛在 root 對(duì)象上,不會(huì)被清除掉。 ////非嚴(yán)格模式下bar會(huì)被定義到全局變量,頁面中的全局變量只有在頁面關(guān)閉后才會(huì)被銷毀 function foo(arg) { bar = "some text"; } //// function foo() { this.var1 = "potential accidental global"; } // Foo 被調(diào)用時(shí), this 指向全局變量(window),意外的創(chuàng)建了全局變量. foo(); -
閉包,閉包會(huì)引用到父級(jí)函數(shù)中的變量,如果閉包未釋放,就會(huì)導(dǎo)致內(nèi)存泄漏。如下代碼是 inner 直接掛在了 root 上,從而導(dǎo)致內(nèi)存泄漏(bigData 不會(huì)釋放)。
function out() { const bigData = new Buffer(100); inner = function () { void bigData; } } -
未銷毀的定時(shí)器和全局函數(shù),如下代碼:如果后續(xù) renderer 元素被移除, 整個(gè)定時(shí)器實(shí)際上沒有任何作用. 但如果你沒有回收定時(shí)器, 整個(gè)定時(shí)器依然有效, 不但定時(shí)器無法被內(nèi)存回收, 定時(shí)器函數(shù)中的依賴也無法回收. 在這個(gè)案例中的 serverData 也無法被回收.
var serverData = loadData(); setInterval(function() { var renderer = document.getElementById('renderer'); if(renderer) { renderer.innerHTML = JSON.stringify(serverData); } }, 5000); // 每 5 秒調(diào)用一次 事件監(jiān)聽
-
- Js中的內(nèi)存管理,參考https://zhuanlan.zhihu.com/p/30552148