金三银四就要到了。整理整理面试题。前端
整篇文章都与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() 复制代码