金三銀四就要到了。整理整理面試題。前端
整篇文章都與JS的函數相關。面試
Function
對象,即: (function(){}).constructor === Function
函數表達式
)高階函數
)閉包
)定義函數的方式有 4 種:算法
new Function(str)
;var fn = function() {}
function fn() {}
var fn = () => {}
PS:new Function
聲明的對象是在函數建立時解析的,故比較低效chrome
MDN的定義:函數
與對其狀態即詞法環境
的引用共同構成閉包(closure)。也就是說,閉包可讓你從內部函數訪問外部函數做用域編程
在JavaScript,函數在每次建立時生成閉包。waht????(MDN說的...)設計模式
小紅書上的更好理解一點:閉包是指有權訪問另一個函數做用域中的變量的函數
數組
也就是說,這就是閉包:安全
function saySomething(){ var name = 'mokou'; return function () { console.log(name); } } var say = saySomething() say() 複製代碼
根據 JS 的垃圾回收機制(不提新生代和老生代),根據可達性算法
:不可達就會被回收。bash
什麼是不可達?簡單來講:堆
內存中沒有在棧
內存中存放引用(即:沒有指針指向堆)就視爲不可達。(不懂堆棧的能夠看下上一篇JS基礎篇)微信
上面案例代碼中:saySomething
方法的返回值的引用存在了 say
變量中,因此可達,故:引用不會被銷燬,從而產生閉包。
案例一:請求出錯的提示框(多個請求同時出錯通常都只有一個提示框)
實現思路:使用傳說中的設計模式 單例模式
如下是單例模式的實現:
const Singleton = (function() { var _instance; return function(obj) { return _instance || (_instance = obj); } })(); var a = new Singleton({x: 1}); var b = new Singleton({y: 2}); console.log(a === b); 複製代碼
PS:上例還有一個優勢:_instance
是私有的,外部不能更改(保證安全無污染/可信)
案例二:解決 var
在 for
+ setTimeout
混合場景中的BUG
BUG 展現:
for (var i=1; i<=5; i++) { setTimeout(function() { console.log(i); }, i*300 ); } 複製代碼
上例會打印:6 6 6 6 6
由於 var
是函數做用域(緣由1),而 setTimeout
是異步執行(緣由2),因此:當 console.log
執行的時候 i
已經等於 6
了(BUG產生)
在沒有 let
和 const
的年代,經常使用的解決方式就是閉包
for (var i = 1; i <= 5; i++) { (function(j) { setTimeout(function() { console.log(j); }, j*300); })(i); } 複製代碼
缺點:
addEventListener
沒有被 removeEventListener
)主要區別在
函數定義
(能夠在函數聲明以前使用)var
定義:有變量聲明提高let 和 const
定義:沒有變量提高JavaScript 中,函數及變量(經過var
方式)的聲明
都將被提高到函數的最頂部。
案例:如下會輸出什麼結果?
var name = 'zmz'; function say(){ var name; console.log(name); var name = 'mokou'; console.log(name); } say(); 複製代碼
答案是:先輸出 undefined
再輸出 mokou
由於在函數 say
內部也聲明瞭一個 name
(是經過 var
)聲明的,因此會聲明提高,可是未賦值,因此首先輸出的是 undefined
,以後是正常流程,輸出 mokou
PS:因爲 var
的變量提高很不友好,因此在 ES6 中添加了 let
和 const
(本章主要講函數,暫略。)
在生成執行上下文時,會有兩個階段。
在提高的過程當中:函數定義優先於變量提高,變量在執行階段纔會被真正賦值。
舉例
console.log(typeof a === 'function') var a = 1; function a() {} console.log(a == 1); 複製代碼
上例會打印 true true
箭頭函數式 ES6 標準
let obj = { x () { let y = () => { console.log(this === obj); } y(); // true // call、apply、bind 都不能改變箭頭函數內部 this 的指向 y.call(window); // true y.apply(window); // true y.bind(window)(); // true // 同時,被bind綁定過的方法,也是不可變的,(不會再次被 bind、call、apply改變this的指向) } } 複製代碼
arguments
,須要手動使用 ...args
參數代替function fn () { console.log(this, 'fn'); function subFn () { console.log(this, 'subFn'); } subFn(); // window } fn(); // window 複製代碼
var x = 'abc'; var obj = { x: 123, fn: function () { console.log(this.x); } } obj.fn(); // 123 var fn = obj.fn; fn(); // abc 複製代碼
call
、apply
、bind
的形式調用(更改指向,箭頭函數除外)// 構造函數中有 return對象 的狀況 function A() { return { a : 1 } } A.prototype.say = function () { console.log(this, 'xx') } var a = new A(); // a = {a: 1} // a.say === undefined // 構造函數中 沒有return對象 的狀況 function A() { // 能夠手動 return this } A.prototype.say = function () { console.log(this, 'xx') } var a = new A(); a.say(); // A {} "xx" 複製代碼
性能測試:如下測試環境爲 chrome v73
function work(a, b, c) {} for (var j = 0; j < 5; j++) { console.time('apply'); for (var i = 0; i < 1000000; i++) { work.apply(this, [1, 2, 3]); } console.timeEnd('apply'); console.time('call'); for (var i = 0; i < 1000000; i++) { work.call(this, 1, 2, 3); } console.timeEnd('call'); } /* // apply: 69.355224609375ms // call: 8.7431640625ms // apply: 57.72119140625ms // call: 4.146728515625ms // apply: 50.552001953125ms // call: 4.12890625ms // apply: 50.242919921875ms // call: 4.720947265625ms // apply: 49.669921875ms // call: 4.054931640625ms */ 複製代碼
測試結果: call 比 apply快 10倍(大約是這樣的)
緣由:.apply
在運行前要對做爲參數的數組進行一系列檢驗和深拷貝,.call
則沒有這些步驟
實現思路
myCall
的第一個參數(暫命名爲that
)做爲 被調用的對象that
上添加一個方法(方法名隨意,暫命名fn
)that[fn](...args)
調用方法(此時this
指向爲that
)具體代碼
Function.prototype.myCall = function(that, ...args) { let func = this; let fn = Symbol("fn"); that[fn] = func; let res = that[fn](...args); delete that[fn]; return res; } 複製代碼
測試一下
function say(x,y,z) { console.log(this.name, x, y, z) } say.myCall({name: 'mokou'}, 1, 2, 3) // 打印 mokou 1 2 3 複製代碼
實現思路
bind
只改變 this
指向,不執行函數,那麼能夠用閉包來實現this
指向的問題能夠借用 call
實現Function.prototype.myBind = function(that) { if (typeof this !== 'function') { throw new TypeError('Error') } const _fn = this; return function(...args) { _fn.call(that, ...args) } } 複製代碼
測試一下:
function say(x,y,z) { console.log(this.name, x, y, z) } const testFn = say.myBind({name: 'mokou'}) testFn(1, 2, 3); // 打印 mokou 1 2 3 複製代碼
PS: 這個小題是半搬運的 @阮一峯 老師的博客
尾遞歸就是:函數最後單純return函數
,尾遞歸來講,因爲只存在一個調用記錄,因此永遠不會發生"棧溢出"錯誤。
ES6出現的尾遞歸,能夠將複雜度O(n)的調用記錄,換爲複雜度O(1)的調用記錄
測試:不使用尾遞歸
function Fibonacci (n) { if ( n <= 1 ) {return 1}; // return 四則運算 return Fibonacci(n - 1) + Fibonacci(n - 2); } Fibonacci(10) // 89 Fibonacci(100) // 超時 Fibonacci(100) // 超時 複製代碼
測試:使用尾遞歸
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) { if( n <= 1 ) {return ac2}; return Fibonacci2 (n - 1, ac2, ac1 + ac2); } Fibonacci2(100) // 573147844013817200000 Fibonacci2(1000) // 7.0330367711422765e+208 Fibonacci2(10000) // Infinity 複製代碼
蹦牀函數(協程):解決遞歸棧溢出問題,將函數變成循環
function trampoline(f) { while (f && f instanceof Function) { f = f(); } return f; } 複製代碼
尾遞歸的優化:
function tco(f) { var value; var active = false; var accumulated = []; return function accumulator() { accumulated.push(arguments); // 除了第一次執行,其餘的執行都是爲了傳參 if (!active) { // 很重要,若是不使用 active關閉後續進入, sum函數超過會溢出 // 在第一次進入進入遞歸優化時激活,關閉後續進入 active = true; // 有參數就執行 while (accumulated.length) { // 調用f,順便清除參數 value = f.apply(this, accumulated.shift()); // 因爲while中又調用 f,f調用sum,而後sum在執行時給accumulated塞了一個參數 // 因此 while循環會在sum返回結果前一種執行,直到遞歸完成 } active = false; return value; } }; } var sum = tco(function(x, y) { if (y > 0) { // 此時的sum是accumulator // 執行sum等於給accumulator傳參 return sum(x + 1, y - 1) } else { return x } }); sum(1, 100000) 複製代碼
for in
遍歷的是對象的可枚舉屬性for of
遍歷的是對象的迭代器屬性forEach
只能遍歷數組,且不能中斷(break等無效)防抖函數:
function debounce(fn, wait) { let timer = null; return (...args) => { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, wait); } } 複製代碼
使用場景:輸入框校驗
節流函數
function throttle(fn, wait = 300) { let flag = true; return (...args) => { if (!flag) return; flag = false; setTimeout(() => { fn.apply(this, args); flag = true; }, wait); } } 複製代碼
使用場景:
onscroll
時觸發的事件once
函數 更合適)ES6 的 class
能夠看做只是一個語法糖,它的絕大部分功能,ES5 均可以作到,新的class寫法只是讓對象原型的寫法更加清晰、更像面向對象編程的語法而已。
new B();
class B {}
// Uncaught ReferenceError: B is not defined
複製代碼
class A { constructor() { this.x = 1; } static say() { return 'zmz'; } print() { console.log(this.x); } } Object.keys(A); // [] Object.keys(A.prototype); // [] 複製代碼
prototype
接例2
console.log(A.say.prototype); // undefined
console.log(new A().print.prototype); // undefined
複製代碼
接例2 A(); // Uncaught TypeError: Class constructor A cannot be invoked without 'new' 複製代碼
class B { x = 1 } // Uncaught SyntaxError: Identifier 'B' has already been declared 複製代碼
須要完成功能
主流繼承方案
function Parent () { this.name = 'mokou'; } function Child() { Parent5.call(this); this.age = '18'; } Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child; 複製代碼
繼承優化(參考 Babel
的降級方案)
function inherits(subClass, superClass) { subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf(subClass, superClass); } 複製代碼
__proto__
指向構造函數的 prototype
this
,指向構造方法詳細代碼
function myNew() { var obj = new Object() var Con = [].shift.call(arguments) obj.__proto__ = Con.prototype var result = Con.apply(obj, arguments) return typeof result === 'object' ? result : obj } 複製代碼
__proto__
, 它是一個訪問器屬性,指向了咱們不能直接訪問到的內部屬性 [[prototype]]
prototype
,每一個實例對象的 __proto__
指向它的構造函數的 prototype
son.__proto__ === Son.prototype
Son.prototype.__proto__ === Parent.prototype
null
。null
沒有原型,並做爲這個原型鏈中的最後一個環節。
son.__proto__.__proto__........ === null
舉例:
class Parent {} class Son extends Parent{} const log = console.log; const son = new Son(); const parent = new Parent(); log(son.constructor === Son) log(son.__proto__ === son.constructor.prototype) log(son.__proto__ === Son.prototype) log(Son.prototype.__proto__ === Parent.prototype) log(Parent.prototype.__proto__ === Object.prototype) log(Object.prototype.__proto__ === null) log(son.__proto__.__proto__.__proto__.__proto__ === null) log(Son.constructor === Function) log(Son.__proto__ === Parent) log(Parent.constructor === Function) log(Parent.__proto__ === Object.__proto__) 複製代碼
PS:因爲 __proto__
的性能問題和兼容性問題,不推薦使用。
推薦
Object.getPrototypeOf
獲取原型屬性Object.setPrototypeOf
修改原型屬性Object.create()
繼承原型PS: for in
和 Object.keys
會調用原型 屬性
靜態屬性/方法:就是不須要實例化類,就能直接調用的 屬性/方法。
綜合上面Parent
和Son
的例子
不論是 son
、Son
仍是Parent
,它們都是對象,因此均可以直接賦值,也能在__proto__
上賦值
因此靜態屬性/方式直接賦值就能夠了
Parent.x = 1
Parent.__proto__.x =2
console.log(Parent.x) // 1
console.log(Parent.__proto__.x) // 2
複製代碼
若是使用 ES6的 Class
定義一個類
class A { constructor() { this.x = 1; } static say() { console.log('zmz'); } print() { console.log(this.x); } } A.say() 複製代碼