今年来,各大公司都缩减了HC,甚至是采起了“裁人”措施,在这样的大环境之下,想要得到一份更好的工做,必然须要付出更多的努力。javascript
本文挑选了20到大厂面试题,你们在阅读时,建议不要先看个人答案,而是本身先思考一番。尽管,本文全部的答案,都是我在翻阅各类资料,思考并验证以后,才给出的。但因水平有限,本人的答案未必是最优的,若是您有更好的答案,欢迎给我留言。css
本文篇幅较长,但愿小伙伴们可以坚持读完,若是想加入交流群,能够经过文末的公众号添加我为好友。html
更多优质文章可戳: https://github.com/YvetteLau/...前端
new
的实现原理:html5
function _new() { let target = {}; //建立的新对象 //第一个参数是构造函数 let [constructor, ...args] = [...arguments]; //执行[[原型]]链接;target 是 constructor 的实例 target.__proto__ = constructor.prototype; //执行构造函数,将属性或方法添加到建立的空对象上 let result = constructor.apply(target, args); if (result && (typeof (result) == "object" || typeof (result) == "function")) { //若是构造函数执行的结构返回的是一个对象,那么返回这个对象 return result; } //若是构造函数返回的不是一个对象,返回建立的新对象 return target; }
若是用一句话说明 this 的指向,那么便是: 谁调用它,this 就指向谁。java
可是仅经过这句话,咱们不少时候并不能准确判断 this 的指向。所以咱们须要借助一些规则去帮助本身:node
this 的指向能够按照如下顺序判断:git
浏览器环境:不管是否在严格模式下,在全局执行环境中(在任何函数体外部)this 都指向全局对象 window
; github
node 环境:不管是否在严格模式下,在全局执行环境中(在任何函数体外部),this 都是空对象 {}
;面试
new
绑定若是是 new
绑定,而且构造函数中没有返回 function 或者是 object,那么 this 指向这个新对象。以下:
构造函数返回值不是 function 或 object。
new Super()
返回的是 this 对象。
function Super(age) { this.age = age; } let instance = new Super('26'); console.log(instance.age); //26
构造函数返回值是 function 或 object,
new Super()
是返回的是Super种返回的对象。
function Super(age) { this.age = age; let obj = {a: '2'}; return obj; } let instance = new Super('hello'); console.log(instance);//{ a: '2' } console.log(instance.age); //undefined
function info(){ console.log(this.age); } var person = { age: 20, info } var age = 28; var info = person.info; info.call(person); //20 info.apply(person); //20 info.bind(person)(); //20
这里一样须要注意一种特殊状况,若是 call,apply 或者 bind 传入的第一个参数值是 undefined
或者 null
,严格模式下 this 的值为传入的值 null /undefined。非严格模式下,实际应用的默认绑定规则,this 指向全局对象(node环境为global,浏览器环境为window)
function info(){ //node环境中:非严格模式 global,严格模式为null //浏览器环境中:非严格模式 window,严格模式为null console.log(this); console.log(this.age); } var person = { age: 20, info } var age = 28; var info = person.info; //严格模式抛出错误; //非严格模式,node下输出undefined(由于全局的age不会挂在 global 上) //非严格模式。浏览器环境下输出 28(由于全局的age会挂在 window 上) info.call(null);
xxx.fn()
function info(){ console.log(this.age); } var person = { age: 20, info } var age = 28; person.info(); //20;执行的是隐式绑定
非严格模式: node环境,执行全局对象 global,浏览器环境,执行全局对象 window。
严格模式:执行 undefined
function info(){ console.log(this.age); } var age = 28; //严格模式;抛错 //非严格模式,node下输出 undefined(由于全局的age不会挂在 global 上) //非严格模式。浏览器环境下输出 28(由于全局的age会挂在 window 上) //严格模式抛出,由于 this 此时是 undefined info();
箭头函数没有本身的this,继承外层上下文绑定的this。
let obj = { age: 20, info: function() { return () => { console.log(this.age); //this继承的是外层上下文绑定的this } } } let person = {age: 28}; let info = obj.info(); info(); //20 let info2 = obj.info.call(person); info2(); //28
深拷贝和浅拷贝是针对复杂数据类型来讲的,浅拷贝只拷贝一层,而深拷贝是层层拷贝。
深拷贝复制变量值,对于非基本类型的变量,则递归至基本类型变量后,再复制。 深拷贝后的对象与原来的对象是彻底隔离的,互不影响,对一个对象的修改并不会影响另外一个对象。
浅拷贝是会将对象的每一个属性进行依次复制,可是当对象的属性值是引用类型时,实质复制的是其引用,当引用指向的值改变时也会跟着变化。
可使用 for in
、 Object.assign
、 扩展运算符 ...
、Array.prototype.slice()
、Array.prototype.concat()
等,例如:
let obj = { name: 'Yvette', age: 18, hobbies: ['reading', 'photography'] } let obj2 = Object.assign({}, obj); let obj3 = {...obj}; obj.name = 'Jack'; obj.hobbies.push('coding'); console.log(obj);//{ name: 'Jack', age: 18,hobbies: [ 'reading', 'photography', 'coding' ] } console.log(obj2);//{ name: 'Yvette', age: 18,hobbies: [ 'reading', 'photography', 'coding' ] } console.log(obj3);//{ name: 'Yvette', age: 18,hobbies: [ 'reading', 'photography', 'coding' ] }
能够看出浅拷贝只最第一层属性进行了拷贝,当第一层的属性值是基本数据类型时,新的对象和原对象互不影响,可是若是第一层的属性值是复杂数据类型,那么新对象和原对象的属性值其指向的是同一块内存地址。
1.深拷贝最简单的实现是:
JSON.parse(JSON.stringify(obj))
JSON.parse(JSON.stringify(obj))
是最简单的实现方式,可是有一些缺陷:
2.实现一个 deepClone 函数
RegExp
或者 Date
类型,返回对应类型function deepClone(obj, hash = new WeakMap()) { //递归拷贝 if (obj instanceof RegExp) return new RegExp(obj); if (obj instanceof Date) return new Date(obj); if (obj === null || typeof obj !== 'object') { //若是不是复杂数据类型,直接返回 return obj; } if (hash.has(obj)) { return hash.get(obj); } /** * 若是obj是数组,那么 obj.constructor 是 [Function: Array] * 若是obj是对象,那么 obj.constructor 是 [Function: Object] */ let t = new obj.constructor(); hash.set(obj, t); for (let key in obj) { //递归 if (obj.hasOwnProperty(key)) {//是不是自身的属性 t[key] = deepClone(obj[key], hash); } } return t; }
call 和 apply 的功能相同,都是改变 this
的执行,并当即执行函数。区别在于传参方式不一样。
func.call(thisArg, arg1, arg2, ...)
:第一个参数是 this
指向的对象,其它参数依次传入。func.apply(thisArg, [argsArray])
:第一个参数是 this
指向的对象,第二个参数是数组或类数组。一块儿思考一下,如何模拟实现 call
?
首先,咱们知道,函数均可以调用 call
,说明 call
是函数原型上的方法,全部的实例均可以调用。即: Function.prototype.call
。
call
方法中获取调用call()
函数window / global
(非严格模式)call
的第一个参数是 this 指向的对象,根据隐式绑定的规则,咱们知道 obj.foo()
, foo()
中的 this
指向 obj
;所以咱们能够这样调用函数 thisArgs.func(...args)
Function.prototype.call = function() { let [thisArg, ...args] = [...arguments]; if (!thisArg) { //context为null或者是undefined thisArg = typeof window === 'undefined' ? global : window; } //this的指向的是当前函数 func (func.call) thisArg.func = this; //执行函数 let result = thisArg.func(...args); delete thisArg.func; //thisArg上并无 func 属性,所以须要移除 return result; }
bind 的实现思路和 call
一致,仅参数处理略有差异。以下:
Function.prototype.apply = function(thisArg, rest) { let result; //函数返回结果 if (!thisArg) { //context为null或者是undefined thisArg = typeof window === 'undefined' ? global : window; } //this的指向的是当前函数 func (func.call) thisArg.func = this; if(!rest) { //第二个参数为 null / undefined result = thisArg.func(); }else { result = thisArg.func(...rest); } delete thisArg.func; //thisArg上并无 func 属性,所以须要移除 return result; }
在开始以前,咱们首先须要搞清楚函数柯里化的概念。
函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,而且返回接受余下的参数并且返回结果的新函数的技术。
const curry = (fn, ...args) => args.length < fn.length //参数长度不足时,从新柯里化该函数,等待接受新参数 ? (...arguments) => curry(fn, ...args, ...arguments) //参数长度知足时,执行函数 : fn(...args);
function sumFn(a, b, c) { return a + b + c; } var sum = curry(sumFn); console.log(sum(2)(3)(5));//10 console.log(sum(2, 3, 5));//10 console.log(sum(2)(3, 5));//10 console.log(sum(2, 3)(5));//10
函数柯里化的主要做用:
- 利用隐式类型转换
==
操做符在左右数据类型不一致时,会先进行隐式转换。
a == 1 && a == 2 && a == 3
的值意味着其不多是基本数据类型。由于若是 a 是 null 或者是 undefined bool类型,都不可能返回true。
所以能够推测 a 是复杂数据类型,JS 中复杂数据类型只有 object
,回忆一下,Object 转换为原始类型会调用什么方法?
[Symbol.toPrimitive]
接口,那么调用此接口,若返回的不是基本数据类型,抛出错误。若是没有部署 [Symbol.toPrimitive]
接口,那么根据要转换的类型,先调用 valueOf
/ toString
hint
是 default
时,调用顺序为:valueOf
>>> toString
,即valueOf
返回的不是基本数据类型,才会继续调用 valueOf
,若是toString
返回的还不是基本数据类型,那么抛出错误。hint
是 string
(Date对象的hint默认是string) ,调用顺序为:toString
>>> valueOf
,即toString
返回的不是基本数据类型,才会继续调用 valueOf
,若是valueOf
返回的还不是基本数据类型,那么抛出错误。hint
是 number
,调用顺序为: valueOf
>>> toString
//部署 [Symbol.toPrimitive] / valueOf/ toString 皆可 //一次返回1,2,3 便可。 let a = { [Symbol.toPrimitive]: (function(hint) { let i = 1; //闭包的特性之一:i 不会被回收 return function() { return i++; } })() }
- 利用数据劫持(Proxy/Object.definedProperty)
let i = 1; let a = new Proxy({}, { i: 1, get: function () { return () => this.i++; } });
- 数组的
toString
接口默认调用数组的join
方法,从新join
方法
let a = [1, 2, 3]; a.join = a.shift;
Box 是 CSS 布局的对象和基本单位,页面是由若干个Box组成的。
元素的类型 和 display
属性,决定了这个 Box 的类型。不一样类型的 Box 会参与不一样的 Formatting Context。
Formatting Context
Formatting Context 是页面的一块渲染区域,而且有一套渲染规则,决定了其子元素将如何定位,以及和其它元素的关系和相互做用。
Formatting Context 有 BFC (Block formatting context),IFC (Inline formatting context),FFC (Flex formatting context) 和 GFC (Grid formatting context)。FFC 和 GFC 为 CC3 中新增。
BFC布局规则
margin
属性决定。属于同一个BFC的两个相邻Box的margin会发生重叠【符合合并原则的margin合并后是使用大的margin】如何建立BFC
BFC 的应用
margin
会发生重叠,触发生成两个BFC,即不会重叠)<script>
标签中增长async
(html5) 或者defer
(html4) 属性,脚本就会异步加载。
<script src="../XXX.js" defer></script>
defer
和 async
的区别在于:
defer
要等到整个页面在内存中正常渲染结束(DOM 结构彻底生成,以及其余脚本执行完成),在window.onload 以前执行;async
一旦下载完,渲染引擎就会中断渲染,执行这个脚本之后,再继续渲染。defer
脚本,会按照它们在页面出现的顺序加载async
脚本不能保证加载顺序
动态建立
script
标签
动态建立的 script
,设置 src
并不会开始下载,而是要添加到文档中,JS文件才会开始下载。
let script = document.createElement('script'); script.src = 'XXX.js'; // 添加到html文件中才会开始下载 document.body.append(script);
XHR 异步加载JS
let xhr = new XMLHttpRequest(); xhr.open("get", "js/xxx.js",true); xhr.send(); xhr.onreadystatechange = function() { if (xhr.readyState == 4 && xhr.status == 200) { eval(xhr.responseText); } }
ES5 有 6 种方式能够实现继承,分别为:
原型链继承的基本思想是利用原型让一个引用类型继承另外一个引用类型的属性和方法。
function SuperType() { this.name = 'Yvette'; this.colors = ['pink', 'blue', 'green']; } SuperType.prototype.getName = function () { return this.name; } function SubType() { this.age = 22; } SubType.prototype = new SuperType(); SubType.prototype.getAge = function() { return this.age; } SubType.prototype.constructor = SubType; let instance1 = new SubType(); instance1.colors.push('yellow'); console.log(instance1.getName()); //'Yvette' console.log(instance1.colors);//[ 'pink', 'blue', 'green', 'yellow' ] let instance2 = new SubType(); console.log(instance2.colors);//[ 'pink', 'blue', 'green', 'yellow' ]
缺点:
借用构造函数的技术,其基本思想为:
在子类型的构造函数中调用超类型构造函数。
function SuperType(name) { this.name = name; this.colors = ['pink', 'blue', 'green']; } function SubType(name) { SuperType.call(this, name); } let instance1 = new SubType('Yvette'); instance1.colors.push('yellow'); console.log(instance1.colors);//['pink', 'blue', 'green', yellow] let instance2 = new SubType('Jack'); console.log(instance2.colors); //['pink', 'blue', 'green']
优势:
缺点:
组合继承指的是将原型链和借用构造函数技术组合到一块,从而发挥两者之长的一种继承模式。基本思路:
使用原型链实现对原型属性和方法的继承,经过借用构造函数来实现对实例属性的继承,既经过在原型上定义方法来实现了函数复用,又保证了每一个实例都有本身的属性。
function SuperType(name) { this.name = name; this.colors = ['pink', 'blue', 'green']; } SuperType.prototype.sayName = function () { console.log(this.name); } function SuberType(name, age) { SuperType.call(this, name); this.age = age; } SuberType.prototype = new SuperType(); SuberType.prototype.constructor = SuberType; SuberType.prototype.sayAge = function () { console.log(this.age); } let instance1 = new SuberType('Yvette', 20); instance1.colors.push('yellow'); console.log(instance1.colors); //[ 'pink', 'blue', 'green', 'yellow' ] instance1.sayName(); //Yvette let instance2 = new SuberType('Jack', 22); console.log(instance2.colors); //[ 'pink', 'blue', 'green' ] instance2.sayName();//Jack
缺点:
优势:
原型继承的基本思想:
借助原型能够基于已有的对象建立新对象,同时还没必要所以建立自定义类型。
function object(o) { function F() { } F.prototype = o; return new F(); }
在 object()
函数内部,先穿甲一个临时性的构造函数,而后将传入的对象做为这个构造函数的原型,最后返回了这个临时类型的一个新实例,从本质上讲,object()
对传入的对象执行了一次浅拷贝。
ECMAScript5经过新增 Object.create()
方法规范了原型式继承。这个方法接收两个参数:一个用做新对象原型的对象和(可选的)一个为新对象定义额外属性的对象(能够覆盖原型对象上的同名属性),在传入一个参数的状况下,Object.create()
和 object()
方法的行为相同。
var person = { name: 'Yvette', hobbies: ['reading', 'photography'] } var person1 = Object.create(person); person1.name = 'Jack'; person1.hobbies.push('coding'); var person2 = Object.create(person); person2.name = 'Echo'; person2.hobbies.push('running'); console.log(person.hobbies);//[ 'reading', 'photography', 'coding', 'running' ] console.log(person1.hobbies);//[ 'reading', 'photography', 'coding', 'running' ]
在没有必要建立构造函数,仅让一个对象与另外一个对象保持类似的状况下,原型式继承是能够胜任的。
缺点:
同原型链实现继承同样,包含引用类型值的属性会被全部实例共享。
寄生式继承是与原型式继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工厂模式相似,即建立一个仅用于封装继承过程的函数,该函数在内部已某种方式来加强对象,最后再像真地是它作了全部工做同样返回对象。
function createAnother(original) { var clone = object(original);//经过调用函数建立一个新对象 clone.sayHi = function () {//以某种方式加强这个对象 console.log('hi'); }; return clone;//返回这个对象 } var person = { name: 'Yvette', hobbies: ['reading', 'photography'] }; var person2 = createAnother(person); person2.sayHi(); //hi
基于 person
返回了一个新对象 -—— person2
,新对象不只具备 person
的全部属性和方法,并且还有本身的 sayHi()
方法。在考虑对象而不是自定义类型和构造函数的状况下,寄生式继承也是一种有用的模式。
缺点:
所谓寄生组合式继承,即经过借用构造函数来继承属性,经过原型链的混成形式来继承方法,基本思路:
没必要为了指定子类型的原型而调用超类型的构造函数,咱们须要的仅是超类型原型的一个副本,本质上就是使用寄生式继承来继承超类型的原型,而后再将结果指定给子类型的原型。寄生组合式继承的基本模式以下所示:
function inheritPrototype(subType, superType) { var prototype = object(superType.prototype); //建立对象 prototype.constructor = subType;//加强对象 subType.prototype = prototype;//指定对象 }
constructor
属性至此,咱们就能够经过调用 inheritPrototype
来替换为子类型原型赋值的语句:
function SuperType(name) { this.name = name; this.colors = ['pink', 'blue', 'green']; } //...code function SuberType(name, age) { SuperType.call(this, name); this.age = age; } SuberType.prototype = new SuperType(); inheritPrototype(SuberType, SuperType); //...code
优势:
只调用了一次超类构造函数,效率更高。避免在SuberType.prototype
上面建立没必要要的、多余的属性,与其同时,原型链还能保持不变。
所以寄生组合继承是引用类型最理性的继承范式。
隐藏类型
屏幕并非惟一的输出机制,好比说屏幕上看不见的元素(隐藏的元素),其中一些依然可以被读屏软件阅读出来(由于读屏软件依赖于可访问性树来阐述)。为了消除它们之间的歧义,咱们将其归为三大类:
彻底隐藏
display
属性display: none;
HTML5 新增属性,至关于 display: none
<div hidden> </div>
视觉上的隐藏
position
和 盒模型 将元素移出可视区范围posoition
为 absolute
或 fixed
,经过设置 top
、left
等值,将其移出可视区域。position:absolute; left: -99999px;
position
为 relative
,经过设置 top
、left
等值,将其移出可视区域。position: relative; left: -99999px; height: 0
margin-left: -99999px; height: 0;
transform: scale(0); height: 0;
translateX
, translateY
transform: translateX(-99999px); height: 0
rotate
transform: rotateY(90deg);
height: 0; width: 0; font-size: 0;
height: 0; width: 0; overflow: hidden;
opacity: 0;
visibility
属性visibility: hidden;
z-index
属性position: relative; z-index: -999;
再设置一个层级较高的元素覆盖在此元素上。
clip-path: polygon(0 0, 0 0, 0 0, 0 0);
语义上的隐藏
读屏软件不可读,占据空间,可见。
<div aria-hidden="true"> </div>
声明方式 | 变量提高 | 暂时性死区 | 重复声明 | 块做用域有效 | 初始值 | 从新赋值 |
---|---|---|---|---|---|---|
var | 会 | 不存在 | 容许 | 不是 | 非必须 | 容许 |
let | 不会 | 存在 | 不容许 | 是 | 非必须 | 容许 |
const | 不会 | 存在 | 不容许 | 是 | 必须 | 不容许 |
1.let/const 定义的变量不会出现变量提高,而 var 定义的变量会提高。
2.相同做用域中,let 和 const 不容许重复声明,var 容许重复声明。
3.const 声明变量时必须设置初始值
4.const 声明一个只读的常量,这个常量不可改变。
这里有一个很是重要的点便是:在JS中,复杂数据类型,存储在栈中的是堆内存的地址,存在栈中的这个地址是不变的,可是存在堆中的值是能够变得。有没有至关常量指针/指针常量~
const a = 20; const b = { age: 18, star: 500 }
一图胜万言,以下图所示,不变的是栈内存中 a 存储的 20,和 b 中存储的 0x0012ff21(瞎编的一个数字)。而 {age: 18, star: 200} 是可变的。
在开始说明JS上下文栈和做用域以前,咱们先说明下JS上下文以及做用域的概念。
执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行。
执行上下文类型分为:
执行上下文建立过程当中,须要作如下几件事:
做用域负责收集和维护由全部声明的标识符(变量)组成的一系列查询,并实施一套很是严格的规则,肯定当前执行的代码对这些标识符的访问权限。—— 摘录自《你不知道的JavaScript》(上卷)
做用域有两种工做模型:词法做用域和动态做用域,JS采用的是词法做用域工做模型,词法做用域意味着做用域是由书写代码时变量和函数声明的位置决定的。(with
和 eval
可以修改词法做用域,可是不推荐使用,对此不作特别说明)
做用域分为:
执行栈,也叫作调用栈,具备 LIFO (后进先出) 结构,用于存储在代码执行期间建立的全部执行上下文。
规则以下:
以一段代码具体说明:
function fun3() { console.log('fun3') } function fun2() { fun3(); } function fun1() { fun2(); } fun1();
Global Execution Context
(即全局执行上下文)首先入栈,过程以下:
伪代码:
//全局执行上下文首先入栈 ECStack.push(globalContext); //执行fun1(); ECStack.push(<fun1> functionContext); //fun1中又调用了fun2; ECStack.push(<fun2> functionContext); //fun2中又调用了fun3; ECStack.push(<fun3> functionContext); //fun3执行完毕 ECStack.pop(); //fun2执行完毕 ECStack.pop(); //fun1执行完毕 ECStack.pop(); //javascript继续顺序执行下面的代码,但ECStack底部始终有一个 全局上下文(globalContext);
做用域链就是从当前做用域开始一层一层向上寻找某个变量,直到找到全局做用域仍是没找到,就宣布放弃。这种一层一层的关系,就是做用域链。
如:
var a = 10; function fn1() { var b = 20; console.log(fn2) function fn2() { a = 20 } return fn2; } fn1()();
fn2做用域链 = [fn2做用域, fn1做用域,全局做用域]
防抖函数的做用
防抖函数的做用就是控制函数在必定时间内的执行次数。防抖意味着N秒内函数只会被执行一次,若是N秒内再次被触发,则从新计算延迟时间。
举例说明: 小思最近在减肥,可是她很是吃吃零食。为此,与其男友约定好,若是10天不吃零食,就能够购买一个包(不要问为何是包,由于包治百病)。可是若是中间吃了一次零食,那么就要从新计算时间,直到小思坚持10天没有吃零食,才能购买一个包。因此,管不住嘴的小思,没有机会买包(悲伤的故事)... 这就是 防抖。
防抖函数实现
timer
是 null
,调用 later()
,若 immediate
为true
,那么当即调用 func.apply(this, params)
;若是 immediate
为 false
,那么过 wait
以后,调用 func.apply(this, params)
timer
已经重置为 null
(即 setTimeout
的倒计时结束),那么流程与第一次触发时同样,若 timer
不为 null
(即 setTimeout 的倒计时未结束),那么清空定时器,从新开始计时。function debounce(func, wait, immediate = true) { let timeout, result; // 延迟执行函数 const later = (context, args) => setTimeout(() => { timeout = null;// 倒计时结束 if (!immediate) { //执行回调 result = func.apply(context, args); context = args = null; } }, wait); let debounced = function (...params) { if (!timeout) { timeout = later(this, params); if (immediate) { //当即执行 result = func.apply(this, params); } } else { clearTimeout(timeout); //函数在每一个等待时延的结束被调用 timeout = later(this, params); } return result; } //提供在外部清空定时器的方法 debounced.cancel = function () { clearTimeout(timer); timer = null; }; return debounced; };
immediate
为 true 时,表示函数在每一个等待时延的开始被调用。immediate
为 false 时,表示函数在每一个等待时延的结束被调用。
防抖的应用场景
节流函数的做用
节流函数的做用是规定一个单位时间,在这个单位时间内最多只能触发一次函数执行,若是这个单位时间内屡次触发函数,只能有一次生效。
节流函数实现
function throttle(func, wait, options = {}) { var timeout, context, args, result; var previous = 0; var later = function () { previous = options.leading === false ? 0 : (Date.now() || new Date().getTime()); timeout = null; result = func.apply(context, args); if (!timeout) context = args = null; }; var throttled = function () { var now = Date.now() || new Date().getTime(); if (!previous && options.leading === false) previous = now; //remaining 为距离下次执行 func 的时间 //remaining > wait,表示客户端系统时间被调整过 var remaining = wait - (now - previous); context = this; args = arguments; //remaining 小于等于0,表示事件触发的间隔时间大于设置的 wait if (remaining <= 0 || remaining > wait) { if (timeout) { //清空定时器 clearTimeout(timeout); timeout = null; } //重置 previous previous = now; //执行函数 result = func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining); } return result; }; throttled.cancel = function () { clearTimeout(timeout); previous = 0; timeout = context = args = null; }; return throttled; }
禁用第一次首先执行,传递 {leading: false}
;想禁用最后一次执行,传递 {trailing: false}
节流的应用场景
《JavaScript高级程序设计》:
闭包是指有权访问另外一个函数做用域中的变量的函数
《JavaScript权威指南》:
从技术的角度讲,全部的JavaScript函数都是闭包:它们都是对象,它们都关联到做用域链。
《你不知道的JavaScript》
当函数能够记住并访问所在的词法做用域时,就产生了闭包,即便函数是在当前词法做用域以外执行。
function foo() { var a = 2; return function fn() { console.log(a); } } let func = foo(); func(); //输出2
闭包使得函数能够继续访问定义时的词法做用域。拜 fn 所赐,在 foo() 执行后,foo 内部做用域不会被销毁。
function base() { let x = 10; //私有变量 return { getX: function() { return x; } } } let obj = base(); console.log(obj.getX()); //10
var a = []; for (var i = 0; i < 10; i++) { a[i] = (function(j){ return function () { console.log(j); } })(i); } a[6](); // 6
function coolModule() { let name = 'Yvette'; let age = 20; function sayName() { console.log(name); } function sayAge() { console.log(age); } return { sayName, sayAge } } let info = coolModule(); info.sayName(); //'Yvette'
模块模式具备两个必备的条件(来自《你不知道的JavaScript》)
在实现 Promise.all 方法以前,咱们首先要知道 Promise.all 的功能和特色,由于在清楚了 Promise.all 功能和特色的状况下,咱们才能进一步去写实现。
Promise.all 功能
Promise.all(iterable)
返回一个新的 Promise 实例。此实例在 iterable
参数内全部的 promise
都 fulfilled
或者参数中不包含 promise
时,状态变成 fulfilled
;若是参数中 promise
有一个失败rejected
,此实例回调失败,失败缘由的是第一个失败 promise
的返回结果。
let p = Promise.all([p1, p2, p3]);
p的状态由 p1,p2,p3决定,分红如下;两种状况:
(1)只有p一、p二、p3的状态都变成 fulfilled
,p的状态才会变成 fulfilled
,此时p一、p二、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p一、p二、p3之中有一个被 rejected
,p的状态就变成 rejected
,此时第一个被reject的实例的返回值,会传递给p的回调函数。
Promise.all 的特色
Promise.all 的返回值是一个 promise 实例
Promise.all
会 同步 返回一个已完成状态的 promise
Promise.all
会 异步 返回一个已完成状态的 promise
Promise.all
返回一个 处理中(pending) 状态的 promise
.Promise.all 返回的 promise 的状态
Promise.all
返回的 promise
异步地变为完成。promise
失败,Promise.all
异步地将失败的那个结果给失败状态的回调函数,而无论其它 promise
是否完成Promise.all
返回的 promise
的完成状态的结果都是一个数组Promise.all 实现
Promise.all = function (promises) { //promises 是可迭代对象,省略参数合法性检查 return new Promise((resolve, reject) => { //Array.from 将可迭代对象转换成数组 promises = Array.from(promises); if (promises.length === 0) { resolve([]); } else { let result = []; let index = 0; for (let i = 0; i < promises.length; i++ ) { //考虑到 i 多是 thenable 对象也多是普通值 Promise.resolve(promises[i]).then(data => { result[i] = data; if (++index === promises.length) { //全部的 promises 状态都是 fulfilled,promise.all返回的实例才变成 fulfilled 态 resolve(result); } }, err => { reject(err); return; }); } } }); }
例如:
flattenDeep([1, [2, [3, [4]], 5]]); //[1, 2, 3, 4, 5]
利用 Array.prototype.flat
ES6 为数组实例新增了 flat
方法,用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数组没有影响。
flat
默认只会 “拉平” 一层,若是想要 “拉平” 多层的嵌套数组,须要给 flat
传递一个整数,表示想要拉平的层数。
function flattenDeep(arr, deepLength) { return arr.flat(deepLength); } console.log(flattenDeep([1, [2, [3, [4]], 5]], 3));
当传递的整数大于数组嵌套的层数时,会将数组拉平为一维数组,JS能表示的最大数字为 Math.pow(2, 53) - 1
,所以咱们能够这样定义 flattenDeep
函数
function flattenDeep(arr) { //固然,大多时候咱们并不会有这么多层级的嵌套 return arr.flat(Math.pow(2,53) - 1); } console.log(flattenDeep([1, [2, [3, [4]], 5]]));
利用 reduce 和 concat
function flattenDeep(arr){ return arr.reduce((acc, val) => Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val), []); } console.log(flattenDeep([1, [2, [3, [4]], 5]]));
使用 stack 无限反嵌套多层嵌套数组
function flattenDeep(input) { const stack = [...input]; const res = []; while (stack.length) { // 使用 pop 从 stack 中取出并移除值 const next = stack.pop(); if (Array.isArray(next)) { // 使用 push 送回内层数组中的元素,不会改动原始输入 original input stack.push(...next); } else { res.push(next); } } // 使用 reverse 恢复原数组的顺序 return res.reverse(); } console.log(flattenDeep([1, [2, [3, [4]], 5]]));
例如:
uniq([1, 2, 3, 5, 3, 2]);//[1, 2, 3, 5]
法1: 利用ES6新增数据类型
Set
Set
相似于数组,可是成员的值都是惟一的,没有重复的值。
function uniq(arry) { return [...new Set(arry)]; }
法2: 利用
indexOf
function uniq(arry) { var result = []; for (var i = 0; i < arry.length; i++) { if (result.indexOf(arry[i]) === -1) { //如 result 中没有 arry[i],则添加到数组中 result.push(arry[i]) } } return result; }
法3: 利用
includes
function uniq(arry) { var result = []; for (var i = 0; i < arry.length; i++) { if (!result.includes(arry[i])) { //如 result 中没有 arry[i],则添加到数组中 result.push(arry[i]) } } return result; }
法4:利用
reduce
function uniq(arry) { return arry.reduce((prev, cur) => prev.includes(cur) ? prev : [...prev, cur], []); }
法5:利用
Map
function uniq(arry) { let map = new Map(); let result = new Array(); for (let i = 0; i < arry.length; i++) { if (map.has(arry[i])) { map.set(arry[i], true); } else { map.set(arry[i], false); result.push(arry[i]); } } return result; }
ES6 规定,默认的 Iterator
接口部署在数据结构的 Symbol.iterator
属性,换个角度,也能够认为,一个数据结构只要具备 Symbol.iterator
属性(Symbol.iterator
方法对应的是遍历器生成函数,返回的是一个遍历器对象),那么就能够其认为是可迭代的。
Symbol.iterator
属性,Symbol.iterator()
返回的是一个遍历器对象for ... of
进行循环Array.from
转换为数组let arry = [1, 2, 3, 4]; let iter = arry[Symbol.iterator](); console.log(iter.next()); //{ value: 1, done: false } console.log(iter.next()); //{ value: 2, done: false } console.log(iter.next()); //{ value: 3, done: false }
Iterator
接口的数据结构:尽管浏览器有同源策略,可是 <script>
标签的 src
属性不会被同源策略所约束,能够获取任意服务器上的脚本并执行。jsonp
经过插入 script
标签的方式来实现跨域,参数只能经过 url
传入,仅能支持 get
请求。
实现原理:
jsonp源码实现
function jsonp({url, params, callback}) { return new Promise((resolve, reject) => { //建立script标签 let script = document.createElement('script'); //将回调函数挂在 window 上 window[callback] = function(data) { resolve(data); //代码执行后,删除插入的script标签 document.body.removeChild(script); } //回调函数加在请求地址上 params = {...params, callback} //wb=b&callback=show let arrs = []; for(let key in params) { arrs.push(`${key}=${params[key]}`); } script.src = `${url}?${arrs.join('&')}`; document.body.appendChild(script); }); }
使用:
function show(data) { console.log(data); } jsonp({ url: 'http://localhost:3000/show', params: { //code }, callback: 'show' }).then(data => { console.log(data); });
服务端代码(node):
//express启动一个后台服务 let express = require('express'); let app = express(); app.get('/show', (req, res) => { let {callback} = req.query; //获取传来的callback函数名,callback是key res.send(`${callback}('Hello!')`); }); app.listen(3000);
[1] [JavaScript高级程序设计第六章]
[2] Step-By-Step】高频面试题深刻解析 / 周刊01
[3] Step-By-Step】高频面试题深刻解析 / 周刊02
[4] Step-By-Step】高频面试题深刻解析 / 周刊03
[5] Step-By-Step】高频面试题深刻解析 / 周刊04
谢谢各位小伙伴愿意花费宝贵的时间阅读本文,若是本文给了您一点帮助或者是启发,请不要吝啬你的赞和Star,您的确定是我前进的最大动力。 https://github.com/YvetteLau/...
推荐关注本人公众号: