今年来,各大公司都缩减了HC,甚至是采取了“裁员”措施,在这样的大环境之下,想要获得一份更好的工作,必然需要付出更多的努力。本文挑选了20道大厂面试题,建议在阅读时,先思考一番,不要直接看解析。如果您有更好的答案,欢迎给我留言。如果有错误,可以在评论区指出。本文篇幅较长,希望小伙伴们能够坚持读完。
new 的实现原理:
如果用一句话说明 this 的指向,那么即是: 谁调用它,this 就指向谁。
但是仅通过这句话,我们很多时候并不能准确判断 this 的指向。因此我们需要借助一些规则去帮助自己:
this 的指向可以按照以下顺序判断:
全局环境中的 this
浏览器环境:无论是否在严格模式下,在全局执行环境中(在任何函数体外部)this 都指向全局对象 window;
node 环境:无论是否在严格模式下,在全局执行环境中(在任何函数体外部),this 都是空对象 {};
是否是 new 绑定
如果是 new 绑定,并且构造函数中没有返回 function 或者是 object,那么 this 指向这个新对象。如下:
构造函数返回值不是 function 或 object。 newSuper() 返回的是 this 对象。
构造函数返回值是 function 或 object, newSuper()是返回的是Super种返回的对象。
函数是否通过 call,apply 调用,或者使用了 bind 绑定,如果是,那么this绑定的就是指定的对象【归结为显式绑定】。
这里同样需要注意一种特殊情况,如果 call,apply 或者 bind 传入的第一个参数值是 undefined或者 null,严格模式下 this 的值为传入的值 null /undefined。非严格模式下,实际应用的默认绑定规则,this 指向全局对象(node环境为global,浏览器环境为window)
隐式绑定,函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。典型的隐式调用为: xxx.fn()
默认绑定,在不能应用其它绑定规则时使用的默认规则,通常是独立函数调用。
非严格模式:node环境,执行全局对象 global,浏览器环境,执行全局对象 window。
箭头函数的情况:
深拷贝和浅拷贝是针对复杂数据类型来说的,浅拷贝只拷贝一层,而深拷贝是层层拷贝。
深拷贝
深拷贝复制变量值,对于非基本类型的变量,则递归至基本类型变量后,再复制。深拷贝后的对象与原来的对象是完全隔离的,互不影响,对一个对象的修改并不会影响另一个对象。
浅拷贝
浅拷贝是会将对象的每个属性进行依次复制,但是当对象的属性值是引用类型时,实质复制的是其引用,当引用指向的值改变时也会跟着变化。
可以使用 forin、 Object.assign、 扩展运算符 … 、 Array.prototype.slice()、 Array.prototype.concat() 等,例如:
可以看出浅拷贝只最第一层属性进行了拷贝,当第一层的属性值是基本数据类型时,新的对象和原对象互不影响,但是如果第一层的属性值是复杂数据类型,那么新对象和原对象的属性值其指向的是同一块内存地址。
深拷贝实现
1.深拷贝最简单的实现是: JSON.parse(JSON.stringify(obj))
JSON.parse(JSON.stringify(obj)) 是最简单的实现方式,但是有一些缺陷:
2.实现一个 deepClone 函数
call 和 apply 的功能相同,都是改变 this 的执行,并立即执行函数。区别在于传参方式不同。
func.call(thisArg,arg1,arg2,…):第一个参数是 this 指向的对象,其它参数依次传入。
func.apply(thisArg,[argsArray]):第一个参数是 this 指向的对象,第二个参数是数组或类数组。
一起思考一下,如何模拟实现 call ?
首先,我们知道,函数都可以调用 call,说明 call 是函数原型上的方法,所有的实例都可以调用。即: Function.prototype.call。
apply 的实现思路和 call 一致,仅参数处理略有差别。如下:
在开始之前,我们首先需要搞清楚函数柯里化的概念。
函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
函数柯里化的主要作用:
1.利用隐式类型转换
== 操作符在左右数据类型不一致时,会先进行隐式转换。
a1&&a2&&a==3的值意味着其不可能是基本数据类型。因为如果 a 是 null 或者是 undefined bool类型,都不可能返回true。
因此可以推测 a 是复杂数据类型,JS 中复杂数据类型只有 object,回忆一下,Object 转换为原始类型会调用什么方法?
如果部署了 [Symbol.toPrimitive] 接口,那么调用此接口,若返回的不是基本数据类型,抛出错误。
如果没有部署 [Symbol.toPrimitive] 接口,那么根据要转换的类型,先调用 valueOf / toString
1.利用数据劫持(Proxy/Object.definedProperty)
2.数组的 toString 接口默认调用数组的 join 方法,重新 join 方法
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布局规则
如何创建BFC
BFC 的应用
script标签中增加 async(html5) 或者 defer(html4) 属性,脚本就会异步加载。
defer 和 async 的区别在于:
动态创建 script 标签
动态创建的 script ,设置 src 并不会开始下载,而是要添加到文档中,JS文件才会开始下载。
XHR 异步加载JS
ES5 有 6 种方式可以实现继承,分别为:
1. 原型链继承
原型链继承的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
缺点:
2. 借用构造函数
借用构造函数的技术,其基本思想为:
优点:
缺点:
3. 组合继承(原型链 + 借用构造函数)
组合继承指的是将原型链和借用构造函数技术组合到一块,从而发挥二者之长的一种继承模式。基本思路:
使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承,既通过在原型上定义方法来实现了函数复用,又保证了每个实例都有自己的属性。
缺点:
优点:
4. 原型式继承
原型继承的基本思想:
借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
在 object() 函数内部,先穿甲一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例,从本质上讲, object() 对传入的对象执行了一次浅拷贝。
ECMAScript5通过新增 Object.create() 方法规范了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象(可以覆盖原型对象上的同名属性),在传入一个参数的情况下, Object.create() 和 object() 方法的行为相同。
在没有必要创建构造函数,仅让一个对象与另一个对象保持相似的情况下,原型式继承是可以胜任的。
缺点:
5. 寄生式继承
寄生式继承是与原型式继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部已某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
基于 person 返回了一个新对象 -—— person2,新对象不仅具有 person 的所有属性和方法,而且还有自己的 sayHi() 方法。在考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。
缺点:
6. 寄生组合式继承
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法,基本思路:
不必为了指定子类型的原型而调用超类型的构造函数,我们需要的仅是超类型原型的一个副本,本质上就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式继承的基本模式如下所示:
至此,我们就可以通过调用 inheritPrototype 来替换为子类型原型赋值的语句:
优点:
因此寄生组合继承是引用类型最理性的继承范式。
隐藏类型
屏幕并不是唯一的输出机制,比如说屏幕上看不见的元素(隐藏的元素),其中一些依然能够被读屏软件阅读出来(因为读屏软件依赖于可访问性树来阐述)。为了消除它们之间的歧义,我们将其归为三大类:
完全隐藏
display 属性
display: none;
hidden 属性
HTML5 新增属性,相当于 display:none
<div
hidden
>
</div>
视觉上的隐藏
1.利用 position 和 盒模型 将元素移出可视区范围
设置 posoition 为 absolute 或 fixed,通过设置 top、 left 等值,将其移出可视区域。
position:absolute;
left: -99999px
;
设置 position 为 relative,通过设置 top、 left 等值,将其移出可视区域。
position: relative;
left: -99999px;
height: 0
设置 margin 值,将其移出可视区域范围(可视区域占位)。
margin-left: -99999px;
height: 0;
2.利用 transfrom
transform: scale(0);
height: 0;
transform: translateX(-99999px);
height: 0
transform: rotateY(90deg);
3.设置其大小为0
height: 0;
width: 0;
font-size: 0;
height: 0;
width: 0;
overflow: hidden;
4.设置透明度为0
opacity: 0;
5. visibility属性
visibility: hidden;
6.层级覆盖, z-index 属性
position: relative;
z-index: -999;
再设置一个层级较高的元素覆盖在此元素上。
7.clip-path 裁剪
clip-path: polygon(0 0, 0 0, 0 0, 0 0);
语义上的隐藏
8.aria-hidden 属性
读屏软件不可读,占据空间,可见。
"true">
1.let/const 定义的变量不会出现变量提升,而 var 定义的变量会提升。
2.相同作用域中,let 和 const 不允许重复声明,var 允许重复声明。
3.const 声明变量时必须设置初始值
4.const 声明一个只读的常量,这个常量不可改变。
这里有一个非常重要的点即是:在JS中,复杂数据类型,存储在栈中的是堆内存的地址,存在栈中的这个地址是不变的,但是存在堆中的值是可以变得。有没有相当常量指针/指针常量~
一图胜万言,如下图所示,不变的是栈内存中 a 存储的 20,和 b 中存储的 0x0012ff21(瞎编的一个数字)。而 {age: 18, star: 200} 是可变的。
在开始说明JS上下文栈和作用域之前,我们先说明下JS上下文以及作用域的概念。
JS执行上下文
执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行。
执行上下文类型分为:
执行上下文创建过程中,需要做以下几件事:
作用域
作用域负责收集和维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。—— 摘录自《你不知道的JavaScript》(上卷)
作用域有两种工作模型:词法作用域和动态作用域,JS采用的是词法作用域工作模型,词法作用域意味着作用域是由书写代码时变量和函数声明的位置决定的。( with 和 eval 能够修改词法作用域,但是不推荐使用,对此不做特别说明)
作用域分为:
JS执行上下文栈(后面简称执行栈)
执行栈,也叫做调用栈,具有 LIFO (后进先出) 结构,用于存储在代码执行期间创建的所有执行上下文。
规则如下:
GlobalExecutionContext (即全局执行上下文)首先入栈,过程如下:
伪代码:
//全局执行上下文首先入栈
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);
作用域链
作用域链就是从当前作用域开始一层一层向上寻找某个变量,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链。
fn2作用域链 = [fn2作用域, fn1作用域,全局作用域]
防抖函数的作用
防抖函数的作用就是控制函数在一定时间内的执行次数。防抖意味着N秒内函数只会被执行一次,如果N秒内再次被触发,则重新计算延迟时间。
举例说明: 小思最近在减肥,但是她非常吃吃零食。为此,与其男朋友约定好,如果10天不吃零食,就可以购买一个包(不要问为什么是包,因为包治百病)。但是如果中间吃了一次零食,那么就要重新计算时间,直到小思坚持10天没有吃零食,才能购买一个包。所以,管不住嘴的小思,没有机会买包(悲伤的故事)… 这就是 防抖。
防抖函数实现
immediate 为 true 时,表示函数在每个等待时延的开始被调用。 immediate 为 false 时,表示函数在每个等待时延的结束被调用。
防抖的应用场景
节流函数的作用
节流函数的作用是规定一个单位时间,在这个单位时间内最多只能触发一次函数执行,如果这个单位时间内多次触发函数,只能有一次生效。
节流函数实现
禁用第一次首先执行,传递 {leading:false} ;想禁用最后一次执行,传递 {trailing:false}
节流的应用场景
闭包的定义
《JavaScript高级程序设计》:
闭包是指有权访问另一个函数作用域中的变量的函数
《JavaScript权威指南》:
从技术的角度讲,所有的JavaScript函数都是闭包:它们都是对象,它们都关联到作用域链。
《你不知道的JavaScript》
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
闭包使得函数可以继续访问定义时的词法作用域。拜 fn 所赐,在 foo() 执行后,foo 内部作用域不会被销毁。
闭包的作用
模块模式具有两个必备的条件(来自《你不知道的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)只有p1、p2、p3的状态都变成 fulfilled,p的状态才会变成 fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被 rejected,p的状态就变成 rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
Promise.all 的特点
Promise.all 的返回值是一个 promise 实例
Promise.all 返回的 promise 的状态
Promise.all 实现
例如:
flattenDeep([1, [2, [3, [4]], 5]]); //[1, 2, 3, 4, 5]
利用 Array.prototype.flat
ES6 为数组实例新增了 flat 方法,用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数组没有影响。
flat 默认只会 “拉平” 一层,如果想要 “拉平” 多层的嵌套数组,需要给 flat 传递一个整数,表示想要拉平的层数。
当传递的整数大于数组嵌套的层数时,会将数组拉平为一维数组,JS能表示的最大数字为 Math.pow(2,53)-1,因此我们可以这样定义 flattenDeep 函数
利用 reduce 和 concat
使用 stack 无限反嵌套多层嵌套数组
例如:
uniq([1, 2, 3, 5, 3, 2]); //[1, 2, 3, 5]
法1: 利用ES6新增数据类型 Set
法2: 利用 indexOf
法3: 利用 includes
法4:利用 reduce
法5:利用 Map
ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,换个角度,也可以认为,一个数据结构只要具有 Symbol.iterator 属性( Symbol.iterator 方法对应的是遍历器生成函数,返回的是一个遍历器对象),那么就可以其认为是可迭代的。
可迭代对象的特点
原生具有 Iterator 接口的数据结构:
尽管浏览器有同源策略,但是 script标签的 src 属性不会被同源策略所约束,可以获取任意服务器上的脚本并执行。 jsonp 通过插入 script 标签的方式来实现跨域,参数只能通过 url 传入,仅能支持 get 请求。
实现原理:
jsonp源码实现
使用:
服务端代码(node):
谢谢各位小伙伴愿意花费宝贵的时间阅读本文,如果本文给了您一点帮助或者是启发,请不要吝啬你的赞,您的肯定是我前进的最大动力。
本文摘自前端迷公众号,如有侵权请通知我,立即删除。