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)
-
返回执行结果
apply
的实现思路和 call
一致,仅参数处理略有差别。如下:
5. 柯里化函数实现
在开始之前,我们首先需要搞清楚函数柯里化的概念。
函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
函数柯里化的主要作用:
-
参数复用
-
提前返回 – 返回接受余下的参数且返回结果的新函数
-
延迟执行 – 返回新函数,等待执行
6. 如何让 (a == 1 && a == 2 && a == 3) 的值为true?
- 利用隐式类型转换
==
操作符在左右数据类型不一致时,会先进行隐式转换。
a==1&&a==2&&a==3
的值意味着其不可能是基本数据类型。因为如果 a 是 null 或者是 undefined bool类型,都不可能返回true。
因此可以推测 a 是复杂数据类型,JS 中复杂数据类型只有 object
,回忆一下,Object 转换为原始类型会调用什么方法?
-
如果部署了
[Symbol.toPrimitive]
接口,那么调用此接口,若返回的不是基本数据类型,抛出错误。 -
如果没有部署
[Symbol.toPrimitive]
接口,那么根据要转换的类型,先调用valueOf
/toString
-
非Date类型对象,
hint
是default
时,调用顺序为:valueOf
>>>toString
,即valueOf
返回的不是基本数据类型,才会继续调用valueOf
,如果toString
返回的还不是基本数据类型,那么抛出错误。 -
如果
hint
是string
(Date对象的hint默认是string) ,调用顺序为:toString
>>>valueOf
,即toString
返回的不是基本数据类型,才会继续调用valueOf
,如果valueOf
返回的还不是基本数据类型,那么抛出错误。 -
如果
hint
是number
,调用顺序为:valueOf
>>>toString
- 利用数据劫持(Proxy/Object.definedProperty)
- 数组的
toString
接口默认调用数组的join
方法,重新join
方法
7. 什么是BFC?BFC的布局规则是什么?如何创建BFC?
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内,两个盒子的垂直距离由
margin
属性决定。属于同一个BFC的两个相邻Box的margin会发生重叠【符合合并原则的margin合并后是使用大的margin】 -
BFC内,每个盒子的左外边缘接触内部盒子的左边缘(对于从右到左的格式,右边缘接触)。即使在存在浮动的情况下也是如此。除非创建新的BFC。
-
BFC的区域不会与float box重叠。
-
BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。
-
计算BFC的高度时,浮动元素也参与计算。
如何创建BFC
-
根元素
-
浮动元素(float 属性不为 none)
-
position 为 absolute 或 fixed
-
overflow 不为 visible 的块元素
-
display 为 inline-block, table-cell, table-caption
BFC 的应用
-
防止 margin 重叠 (同一个BFC内的两个两个相邻Box的
margin
会发生重叠,触发生成两个BFC,即不会重叠) -
清除内部浮动 (创建一个新的 BFC,因为根据 BFC 的规则,计算 BFC 的高度时,浮动元素也参与计算)
-
自适应多栏布局 (BFC的区域不会与float box重叠。因此,可以触发生成一个新的BFC)
8. 异步加载JS脚本的方式有哪些?
<script>
标签中增加async
(html5) 或者defer
(html4) 属性,脚本就会异步加载。
<scriptsrc="../XXX.js"defer></script>
defer
和 async
的区别在于:
-
defer
要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),在window.onload 之前执行; -
async
一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。 -
如果有多个
defer
脚本,会按照它们在页面出现的顺序加载 -
多个
async
脚本不能保证加载顺序
动态创建
script
标签
动态创建的 script
,设置 src
并不会开始下载,而是要添加到文档中,JS文件才会开始下载。
XHR 异步加载JS
9. ES5有几种方式可以实现继承?分别有哪些优缺点?
ES5 有 6 种方式可以实现继承,分别为:
1. 原型链继承
原型链继承的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
缺点:
-
通过原型来实现继承时,原型会变成另一个类型的实例,原先的实例属性变成了现在的原型属性,该原型的引用类型属性会被所有的实例共享。
-
在创建子类型的实例时,没有办法在不影响所有对象实例的情况下给超类型的构造函数中传递参数。
2. 借用构造函数
借用构造函数的技术,其基本思想为:
在子类型的构造函数中调用超类型构造函数。
优点:
-
可以向超类传递参数
-
解决了原型中包含引用类型值被所有实例共享的问题
缺点:
- 方法都在构造函数中定义,函数复用无从谈起,另外超类型原型中定义的方法对于子类型而言都是不可见的。
3. 组合继承(原型链 + 借用构造函数)
组合继承指的是将原型链和借用构造函数技术组合到一块,从而发挥二者之长的一种继承模式。基本思路:
使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承,既通过在原型上定义方法来实现了函数复用,又保证了每个实例都有自己的属性。
缺点:
- 无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。
优点:
-
可以向超类传递参数
-
每个实例都有自己的属性
-
实现了函数复用
4. 原型式继承
原型继承的基本思想:
借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
在 object()
函数内部,先穿甲一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例,从本质上讲, object()
对传入的对象执行了一次浅拷贝。
ECMAScript5通过新增 Object.create()
方法规范了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象(可以覆盖原型对象上的同名属性),在传入一个参数的情况下, Object.create()
和 object()
方法的行为相同。
在没有必要创建构造函数,仅让一个对象与另一个对象保持相似的情况下,原型式继承是可以胜任的。
缺点:
同原型链实现继承一样,包含引用类型值的属性会被所有实例共享。
5. 寄生式继承
寄生式继承是与原型式继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部已某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
基于 person
返回了一个新对象 -—— person2
,新对象不仅具有 person
的所有属性和方法,而且还有自己的 sayHi()
方法。在考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。
缺点:
-
使用寄生式继承来为对象添加函数,会由于不能做到函数复用而效率低下。
-
同原型链实现继承一样,包含引用类型值的属性会被所有实例共享。
6. 寄生组合式继承
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法,基本思路:
不必为了指定子类型的原型而调用超类型的构造函数,我们需要的仅是超类型原型的一个副本,本质上就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式继承的基本模式如下所示:
-
第一步:创建超类型原型的一个副本
-
第二步:为创建的副本添加
constructor
属性 -
第三步:将新创建的对象赋值给子类型的原型
至此,我们就可以通过调用 inheritPrototype
来替换为子类型原型赋值的语句:
优点:
只调用了一次超类构造函数,效率更高。避免在 SuberType.prototype
上面创建不必要的、多余的属性,与其同时,原型链还能保持不变。
因此寄生组合继承是引用类型最理性的继承范式。
10. 隐藏页面中的某个元素的方法有哪些?
隐藏类型
屏幕并不是唯一的输出机制,比如说屏幕上看不见的元素(隐藏的元素),其中一些依然能够被读屏软件阅读出来(因为读屏软件依赖于可访问性树来阐述)。为了消除它们之间的歧义,我们将其归为三大类:
-
完全隐藏:元素从渲染树中消失,不占据空间。
-
视觉上的隐藏:屏幕中不可见,占据空间。
-
语义上的隐藏:读屏软件不可读,但正常占据空。
完全隐藏
1. display
属性
display: none;
2.hidden 属性
HTML5 新增属性,相当于 display:none
视觉上的隐藏
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;
- 移动
translateX
,translateY
transform: translateX(-99999px);
height: 0
- 旋转
rotate
transform: rotateY(90deg);
3.设置其大小为0
- 宽高为0,字体大小为0:
height: 0;
width: 0;
font-size: 0;
- 宽高为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);
语义上的隐藏
aria-hidden 属性
读屏软件不可读,占据空间,可见。
11. let、const、var 的区别有哪些?
1.let/const 定义的变量不会出现变量提升,而 var 定义的变量会提升。
2.相同作用域中,let 和 const 不允许重复声明,var 允许重复声明。
3.const 声明变量时必须设置初始值
4.const 声明一个只读的常量,这个常量不可改变。
这里有一个非常重要的点即是:在JS中,复杂数据类型,存储在栈中的是堆内存的地址,存在栈中的这个地址是不变的,但是存在堆中的值是可以变得。有没有相当常量指针/指针常量~
一图胜万言,如下图所示,不变的是栈内存中 a 存储的 20,和 b 中存储的 0x0012ff21(瞎编的一个数字)。而 {age: 18, star: 200} 是可变的。
12. 说一说你对JS执行上下文栈和作用域链的理解?
在开始说明JS上下文栈和作用域之前,我们先说明下JS上下文以及作用域的概念。
JS执行上下文
执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行。
执行上下文类型分为:
-
全局执行上下文
-
函数执行上下文
执行上下文创建过程中,需要做以下几件事:
-
创建变量对象:首先初始化函数的参数arguments,提升函数声明和变量声明。
-
创建作用域链(Scope Chain):在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。
-
确定this的值,即 ResolveThisBinding
作用域
作用域负责收集和维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。—— 摘录自《你不知道的JavaScript》(上卷)
作用域有两种工作模型:词法作用域和动态作用域,JS采用的是词法作用域工作模型,词法作用域意味着作用域是由书写代码时变量和函数声明的位置决定的。( with
和 eval
能够修改词法作用域,但是不推荐使用,对此不做特别说明)
作用域分为:
-
全局作用域
-
函数作用域
-
块级作用域
JS执行上下文栈(后面简称执行栈)
执行栈,也叫做调用栈,具有 LIFO (后进先出) 结构,用于存储在代码执行期间创建的所有执行上下文。
规则如下:
-
首次运行JavaScript代码的时候,会创建一个全局执行的上下文并Push到当前的执行栈中,每当发生函数调用,引擎都会为该函数创建一个新的函数执行上下文并Push当前执行栈的栈顶。
-
当栈顶的函数运行完成后,其对应的函数执行上下文将会从执行栈中Pop出,上下文的控制权将移动到当前执行栈的下一个执行上下文。
以一段代码具体说明:
GlobalExecutionContext
(即全局执行上下文)首先入栈,过程如下:
伪代码:
//全局执行上下文首先入栈
ECStack.push(globalContext);
//执行fun1();
ECStack.push( functionContext);
//fun1中又调用了fun2;
ECStack.push( functionContext);
//fun2中又调用了fun3;
ECStack.push( functionContext);
//fun3执行完毕
ECStack.pop();
//fun2执行完毕
ECStack.pop();
//fun1执行完毕
ECStack.pop();
//javascript继续顺序执行下面的代码,但ECStack底部始终有一个 全局上下文(globalContext);
作用域链
作用域链就是从当前作用域开始一层一层向上寻找某个变量,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链。
如:
fn2作用域链 = [fn2作用域, fn1作用域,全局作用域]
13. 防抖函数的作用是什么?请实现一个防抖函数
防抖函数的作用
防抖函数的作用就是控制函数在一定时间内的执行次数。防抖意味着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 的倒计时未结束),那么清空定时器,重新开始计时。
immediate
为 true 时,表示函数在每个等待时延的开始被调用。 immediate
为 false 时,表示函数在每个等待时延的结束被调用。
防抖的应用场景
-
搜索框输入查询,如果用户一直在输入中,没有必要不停地调用去请求服务端接口,等用户停止输入的时候,再调用,设置一个合适的时间间隔,有效减轻服务端压力。
-
表单验证
-
按钮提交事件。
-
浏览器窗口缩放,resize事件(如窗口停止改变大小之后重新计算布局)等。
14. 节流函数的作用是什么?有哪些应用场景,请实现一个节流函数
节流函数的作用
节流函数的作用是规定一个单位时间,在这个单位时间内最多只能触发一次函数执行,如果这个单位时间内多次触发函数,只能有一次生效。
节流函数实现
禁用第一次首先执行,传递 {leading:false}
;想禁用最后一次执行,传递 {trailing:false}
节流的应用场景
-
按钮点击事件
-
拖拽事件
-
onScoll
-
计算鼠标移动的距离(mousemove)
15. 什么是闭包?闭包的作用是什么?
闭包的定义
《JavaScript高级程序设计》:
闭包是指有权访问另一个函数作用域中的变量的函数
《JavaScript权威指南》:
从技术的角度讲,所有的JavaScript函数都是闭包:它们都是对象,它们都关联到作用域链。
《你不知道的JavaScript》
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
创建一个闭包
闭包使得函数可以继续访问定义时的词法作用域。拜 fn 所赐,在 foo() 执行后,foo 内部作用域不会被销毁。
闭包的作用
-
能够访问函数定义时所在的词法作用域(阻止其被回收)。
-
私有化变量
- 模拟块级作用域
- 创建模块
模块模式具有两个必备的条件(来自《你不知道的JavaScript》)
-
必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)
-
封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
16. 实现 Promise.all 方法
在实现 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]);
JavaScript
-
js的基本类型有哪些?引用类型有哪些?null和undefined的区别。
-
如何判断一个变量是Array类型?如何判断一个变量是Number类型?(都不止一种)
-
Object是引用类型嘛?引用类型和基本类型有什么区别?哪个是存在堆哪一个是存在栈上面的?
-
JS常见的dom操作api
-
解释一下事件冒泡和事件捕获
-
事件委托(手写例子),事件冒泡和捕获,如何阻止冒泡?如何组织默认事件?
-
对闭包的理解?什么时候构成闭包?闭包的实现方法?闭包的优缺点?
-
this有哪些使用场景?跟C,Java中的this有什么区别?如何改变this的值?
-
call,apply,bind
-
显示原型和隐式原型,手绘原型链,原型链是什么?为什么要有原型链
-
创建对象的多种方式
-
实现继承的多种方式和优缺点
-
new 一个对象具体做了什么
-
手写Ajax,XMLHttpRequest
-
变量提升
-
举例说明一个匿名函数的典型用例
-
指出JS的宿主对象和原生对象的区别,为什么扩展JS内置对象不是好的做法?有哪些内置对象和内置函数?
-
attribute和property的区别
-
document load和document DOMContentLoaded两个事件的区别
-
JS代码调试