JS相關
1.變數提升
ES6之前我們一般使用var來宣告變數,提升簡單來說就是把我們所寫的類似於var a = 123;這樣的程式碼,宣告提升到它所在作用域的頂端去執行,到我們程式碼所在的位置來賦值。
function test() { console.log(a); // undefined a = 123; }
test();
執行順序如下:
function test() { var a; console.log(a); // undefined a = 123; } test();
2.函式提升
javascript中不僅僅是變數宣告有提升的現象,函式的宣告也是一樣;具名函式的宣告有兩種方式:1. 函式宣告式 2. 函式字面量式
function test() {} // 函式式宣告 let test = function() {} // 字面量宣告
函式提升是整個程式碼塊提升到它所在的作用域的最開始執行
console.log(f); function f() { console.log(1); } // 相當於以下程式碼 function f() { console.log(1); } console.log(f);
foo(); //1 var foo; function foo () { console.log(1); } foo = function () { console.log(2); }
根因分析:javascript引擎並將var a和a = 2看做是兩個單獨的宣告,第一個是編譯階段的任務,而第二個則是執行階段的任務。這意味著無論作用域中的宣告出現在什麼地方,都將在程式碼本身被執行前首先進行處理,可以將這個過程形象地想象成所有的宣告(變數和函式)都會被“移動”到各自作用域的最頂端,這個過程被稱為提升。
3.bind、call、apply
call和apply其實是同一個東西,區別只有引數不同,call是apply的語法糖,所以就放在一起說了,這兩個方法都是定義在函式物件的原型上的(Function.prototype),call和apply方法的作用都是改變函式的執行環境,第一個引數傳入上下文執行環境,然後傳入函式執行所需的引數。傳入call的引數只能是單個引數,不能是陣列。apply可傳入陣列。話不多說直接上程式碼,看下面的例子:
function ga() { let x=1;
} function gb(y) { return x+y; } gb(2) //呼叫發生報錯,因為拿不到x的值 gb.call(ga,2); //使gb在ga環境中執行,可以拿到x,執行正常
上面的程式碼中由於gb()函式執行依賴於ga()中的變數,所以我們使用了call將gb的執行環境變成了ga。
function gg(x,y,z){ let a=Array.prototype.slice.call(arguments,1,2) //通過slice方法獲取到了第二個引數 return a; //返回[2] } gg(1,2,3)
// arguments是一個類陣列物件,它本身不能呼叫陣列的slice方法,使用call將執行slice方法的物件由陣列變為了arguments。
使用apply改寫上面的方法
function gg(x,y,z){ let d=[1,2] let a=Array.prototype.slice.apply(arguments,d) //通過slice方法獲取到了第二個引數 return a; //返回[2] } gg(1,2,3)
使用apply和call實現繼承
function Parent(name) { this.name = name; this.sayHello = function() { alert(name); } } function Child(name) { // 子類的this傳給父類 Parent.call(this, name); } let parent = new Parent("張三"); let child = new Child("李四"); parent.sayHello(); child.sayHello();
bind和apply區別是apply會立刻執行,而bind只是起一個繫結執行上下文的作用。看下面的例子:
function ga() { let x=1; (function gb(y) { return x+y; }).bind(this) //使用bind將gb函式的執行上下文繫結到ga上 } gb(2) //執行正常,得到3 // 有些情況下為了方便我們可以直接將ga繫結,而不用在呼叫的時候再使用apply。
4.原型&原型鏈
在JavaScript中,每個函式都有一個prototype屬性,這個屬性指向函式的原型物件(原型就是一個Object的例項,是一個物件)
每個物件(除null外)都會有的屬性,叫做__proto__,這個屬性會指向該物件的原型;絕大部分瀏覽器都支援這個非標準的方法訪問原型,然而它並不存在於 Person.prototype 中,實際上,它是來自於 Object.prototype ,與其說是一個屬性,不如說是一個 getter/setter,當使用 obj.__proto__ 時,可以理解成返回了 Object.getPrototypeOf(obj)。
每個原型都有一個constructor屬性,指向該關聯的建構函式
當讀取例項的屬性時,如果找不到,就會查詢與物件關聯的原型中的屬性,如果還查不到,就去找原型的原型,一直找到最頂層為止
原型的原型是什麼?
其實原型物件就是通過 Object 建構函式生成的,結合之前所講,例項的 __proto__ 指向建構函式的 prototype
簡單的回顧一下建構函式、原型和例項的關係:每個建構函式都有一個原型物件,原型物件都包含一個指向建構函式的指標,而例項都包含一個指向原型物件的內部指標。那麼假如我們讓原型物件等於另一個型別的例項,結果會怎樣?顯然,此時的原型物件將包含一個指向另一個原型的指標,相應地,另一個原型中也包含著一個指向另一個建構函式的指標。假如另一個原型又是另一個型別的例項,那麼上述關係依然成立。如此層層遞進,就構成了例項與原型的鏈條。這就是所謂的原型鏈的基本概念。
如圖所示:藍色即為原型鏈。
5. this指向
面嚮物件語言中 this 表示當前物件的一個引用。
但在 JavaScript 中 this 不是固定不變的,它會隨著執行環境的改變而改變。
- 在方法中,this 表示該方法所屬的物件。
- 如果單獨使用,this 表示全域性物件。
- 在函式中,this 表示全域性物件。
- 在函式中,在嚴格模式下,this 是未定義的(undefined)。
- 在事件中,this 表示接收事件的元素。
- 類似 call() 和 apply() 方法可以將 this 引用到任何物件
function foo() { console.log(this.a) } var a = 1 foo() var obj = { a: 2, foo: foo } obj.foo() // 以上兩者情況 `this` 只依賴於呼叫函式前的物件,優先順序是第二個情況大於第一個情況 // 以下情況是優先順序最高的,`this` 只會繫結在 `c` 上,不會被任何方式修改 `this` 指向 var c = new foo() c.a = 3 console.log(c.a) 1 2 undefined 3
6.堆和棧
這裡先說兩個概念:1、堆(heap)2、棧(stack)
堆 是堆記憶體的簡稱。
棧 是棧記憶體的簡稱。
說到堆疊,我們講的就是記憶體的使用和分配了,沒有暫存器的事,也沒有硬碟的事。
各種語言在處理堆疊的原理上都大同小異。堆是動態分配記憶體,記憶體大小不一,也不會自動釋放。棧是自動分配相對固定大小的記憶體空間,並由系統自動釋放。
javascript的基本型別就5種:Undefined、Null、Boolean、Number和String,它們都是直接按值儲存在棧中的,每種型別的資料佔用的記憶體空間的大小是確定的,並由系統自動分配和自動釋放。這樣帶來的好處就是,記憶體可以及時得到回收,相對於堆來說,更加容易管理記憶體空間。
javascript中其他型別的資料被稱為引用型別的資料 : 如物件(Object)、陣列(Array)、函式(Function) …,它們是通過拷貝和new出來的,這樣的資料儲存於堆中。其實,說儲存於堆中,也不太準確,因為,引用型別的資料的地址指標是儲存於棧中的,當我們想要訪問引用型別的值的時候,需要先從棧中獲得物件的地址指標,然後,在通過地址指標找到堆中的所需要的資料。
說來也是形象,棧,線性結構,後進先出,便於管理。堆,一個混沌,雜亂無章,方便儲存和開闢記憶體空間;
7.generate,async, await 參考https://blog.csdn.net/qdmoment/article/details/86672907
generator生成器的設計原理:
- 狀態機,簡化函式內部狀態儲存;
- 半協程實現
- 上下文凍結
應用場景:
- 非同步操作的同步化表達
- 控制流管理
- 部署 Iterator 介面
- 作為資料結構
整個 Generator 函式就是一個封裝的非同步任務,或者說是非同步任務的容器。非同步操作需要暫停的地方,都用yield
語句註明
Generator 函式是協程在 ES6 的實現,最大特點就是可以交出函式的執行權(即暫停執行)
generator生成器和iterator遍歷器是對應的,我們知道iterator遍歷器是給不同資料結構提供統一的資料介面機制,那麼相對的generator生成器是生成這樣一個遍歷器,進而使資料結構擁有iterator遍歷器介面。換一種方法來說,generator函式提供了可供遍歷的狀態,所以generator是一個狀態機,在其內部封裝了多個狀態,這些狀態可以使用iterator遍歷器遍歷。
注意:既然generator是一個狀態機,所以直接執行generator()函式,並不會執行,相反的是生成一個指向內部狀態的指標物件,即一個可供遍歷的遍歷器。
想執行generator,必須呼叫遍歷器物件的next方法,使得指標移向下一個狀態,直到遇到下一個yield表示式(或return語句)為止。Generator 函式是分段執行的,yield表示式是暫停執行的標記,而next方法可以恢復執行。
const test = testGen(); test.next() // { value: '1', done: false } test.next() // { value: '2', done: false } test.next() // { value: 'ending', done: true } test.next() // { value: undefined, done: true } // 函式有三個狀態 1,2,return function* testGen() { yield '1'; yield '2'; return 'end'; }
Generator的原型方法:
Generator.prototype.throw(),Generator.prototype.return()
throw() 在函式體外丟擲錯誤,然後在 Generator 函式體內捕獲
return():返回給定的值,並且終結遍歷 Generator 函式
next()、throw()、return() 的共同點
作用都是讓 Generator 函式恢復執行,並且使用不同的語句替換yield表示式(帶入參)
next()是將yield表示式替換成一個值
throw()是將yield表示式替換成一個throw語句
return()是將yield表示式替換成一個return語句
async函式
async 函式的實現原理,就是將 Generator 函式和自動執行器,包裝在一個函式裡。
(看了很多遍還不是很明白~)
async function fn(args) { // ... } // 等同於 function fn(args) { return spawn(function* () { // ... }); } function spawn(genF) { return new Promise(function(resolve, reject) { const gen = genF(); function step(nextF) { let next; try { next = nextF(); } catch(e) { return reject(e); } if(next.done) { return resolve(next.value); } Promise.resolve(next.value).then(function(v) { step(function() { return gen.next(v); }); }, function(e) { step(function() { return gen.throw(e); }); }); } step(function() { return gen.next(undefined); }); }); }
8.如何實現一個 Promise
promise的核心原理其實就是釋出訂閱模式,通過兩個佇列來快取成功的回撥(onResolve)和失敗的回撥(onReject)。
promise的特點:
- new Promise時需要傳遞一個executor執行器,執行器會立刻執行(是在主執行緒執行,區別於then)
- 執行器中傳遞了兩個引數:resolve成功的函式、reject失敗的函式,他們呼叫時可以接受任何值的引數value
- promise狀態只能從pending態轉onfulfilled,onrejected到resolved或者rejected,然後執行相應快取佇列中的任務
- promise例項,每個例項都有一個then方法,這個方法傳遞兩個引數,一個是成功回撥onfulfilled,另一個是失敗回撥onrejected
- promise例項呼叫then時,如果狀態resolved,會讓onfulfilled執行並且把成功的內容當作引數傳遞到函式中
- promise中可以同一個例項then多次,如果狀態是pengding 需要將函式存放起來 等待狀態確定後 在依次將對應的函式執行 (釋出訂閱)
(1) 建構函式
function Promise(resolver) {}
(2) 原型方法
Promise.prototype.then = function() {}
Promise.prototype.catch = function() {}
(3) 靜態方法
Promise.resolve = function() {}
Promise.reject = function() {}
Promise.all = function() {}
Promise.race = function() {}
function Promise (executor) { var self = this;//resolve和reject中的this指向不是promise例項,需要用self快取 self.state = 'padding'; self.value = '';//快取成功回撥onfulfilled的引數 self.reson = '';//快取失敗回撥onrejected的引數 self.onResolved = []; // 專門存放成功的回撥onfulfilled的集合 self.onRejected = []; // 專門存放失敗的回撥onrejected的集合 function resolve (value) { if(self.state==='padding'){ self.state==='resolved'; self.value=value; self.onResolved.forEach(fn=>fn()) } } function reject (reason) { self.state = 'rejected'; self.value = reason; self.onRejected.forEach(fn=>fn()) } try{ executor(resolve,reject) }catch(e){ reject(e) } } Promise.prototype.then=function (onfulfilled,onrejected) { var self=this; if(this.state==='resolved'){ onfulfilled(self.value) } if(this.state==='rejected'){ onrejected(self.value) } if(this.state==='padding'){ this.onResolved.push(function () { onfulfilled(self.value) }) } } Promise.prototype.catch = function (onrejected) { return this.then(null, onrejected) }; Promise.reject = function (reason) { return new Promise((resolve, reject) => { reject(reason) }) }; Promise.resolve = function (value) { return new Promise((resolve, reject) => { resolve(value); }) }; Promise.all=function (promises) { return new Promise((resolve,reject)=>{ let results=[],i=0; for(let i=0;i<promises.length;i++){ let p=promises[i]; p.then((data)=>{ processData(i,data) },reject) } function processData (index,data) { results[index]=data; if(++i==promises.length){ resolve(results) } } }) }; //在每個promise的回撥中新增一個resolve(就是在當前的promise.then中新增),有一個狀態改變,就讓race的狀態改變 Promise.race=function (promises) { return new promises((resolve,reject)=>{ for(let i=0;i<promises.length;i++){ let p=promises[i]; p.then(resolve,reject) } })
9.垃圾回收機制
一般來說沒有被引用的物件就是垃圾,就是要被清除, 有個例外如果幾個物件引用形成一個環,互相引用,但根訪問不到它們,這幾個物件也是垃圾,也要被清除。
JS中最常見的垃圾回收方式是標記清除。
工作原理:是當變數進入環境時,將這個變數標記為“進入環境”。當變數離開環境時,則將其標記為“離開環境”。標記“離開環境”的就回收記憶體。
工作流程:
1. 垃圾回收器,在執行的時候會給儲存在記憶體中的所有變數都加上標記。
2. 去掉環境中的變數以及被環境中的變數引用的變數的標記。
3. 再被加上標記的會被視為準備刪除的變數。
4. 垃圾回收器完成記憶體清除工作,銷燬那些帶標記的值並回收他們所佔用的記憶體空間。
引用計數 方式
工作原理:跟蹤記錄每個值被引用的次數。
工作流程:
1. 宣告瞭一個變數並將一個引用型別的值賦值給這個變數,這個引用型別值的引用次數就是1。
2. 同一個值又被賦值給另一個變數,這個引用型別值的引用次數加1.
3. 當包含這個引用型別值的變數又被賦值成另一個值了,那麼這個引用型別值的引用次數減1.
4. 當引用次數變成0時,說明沒辦法訪問這個值了。
5. 當垃圾收集器下一次執行時,它就會釋放引用次數是0的值所佔的記憶體。
新生代演算法(http://newhtml.net/v8-garbage-collection/)
新生代中的物件一般存活時間較短,使用 Scavenge GC 演算法。
在新生代空間中,記憶體空間分為兩部分,分別為 From 空間和 To 空間。在這兩個空間中,必定有一個空間是使用的,另一個空間是空閒的。新分配的物件會被放入 From 空間中,當 From 空間被佔滿時,新生代 GC 就會啟動了。演算法會檢查 From 空間中存活的物件並複製到 To 空間中,如果有失活的物件就會銷燬。當複製完成後將 From 空間和 To 空間互換,這樣 GC 就結束了。
老生代演算法
老生代中的物件一般存活時間較長且數量也多,使用了兩個演算法,分別是標記清除演算法和標記壓縮演算法。
在講演算法前,先來說下什麼情況下物件會出現在老生代空間中:
- 新生代中的物件是否已經經歷過一次 Scavenge 演算法,如果經歷過的話,會將物件從新生代空間移到老生代空間中。
- To 空間的物件佔比大小超過 25 %。在這種情況下,為了不影響到記憶體分配,會將物件從新生代空間移到老生代空間中。
10. 深拷貝
這個問題通常可以通過 JSON.parse(JSON.stringify(object))
來解決。
但是該方法也是有侷限性的:
- 會忽略
undefined
- 會忽略
symbol
- 不能序列化函式
- 不能解決迴圈引用的物件
手動實現:
// 定義一個深拷貝函式 接收目標target引數 function deepClone(target) { // 定義一個變數 let result; // 如果當前需要深拷貝的是一個物件的話 if (typeof target === 'object') { // 如果是一個陣列的話 if (Array.isArray(target)) { result = []; // 將result賦值為一個陣列,並且執行遍歷 for (let i in target) { // 遞迴克隆陣列中的每一項 result.push(deepClone(target[i])) } // 判斷如果當前的值是null的話;直接賦值為null } else if(target===null) { result = null; // 判斷如果當前的值是一個RegExp物件的話,直接賦值 } else if(target.constructor===RegExp){ result = target; }else { // 否則是普通物件,直接for in迴圈,遞迴賦值物件的所有值 result = {}; for (let i in target) { result[i] = deepClone(target[i]); } } // 如果不是物件的話,就是基本資料型別,那麼直接賦值 } else { result = target; } // 返回最終結果 return result; }