京东前端一面面试题
对 JSON 的理解
JSON 是一种基于文本的轻量级的数据交换格式。它可以被任何的编程语言读取和作为数据格式来传递。
在项目开发中,使用 JSON 作为前后端数据交换的方式。在前端通过将一个符合 JSON 格式的数据结构序列化为 JSON 字符串,然后将它传递到后端,后端通过 JSON 格式的字符串解析后生成对应的数据结构,以此来实现前后端数据的一个传递。
因为 JSON 的语法是基于 js 的,因此很容易将 JSON 和 js 中的对象弄混,但是应该注意的是 JSON 和 js 中的对象不是一回事,JSON 中对象格式更加严格,比如说在 JSON 中属性值不能为函数,不能出现 NaN 这样的属性值等,因此大多数的 js 对象是不符合 JSON 对象的格式的。
在 js 中提供了两个函数来实现 js 数据结构和 JSON 格式的转换处理,
JSON.stringify 函数,通过传入一个符合 JSON 格式的数据结构,将其转换为一个 JSON 字符串。如果传入的数据结构不符合 JSON 格式,那么在序列化的时候会对这些值进行对应的特殊处理,使其符合规范。在前端向后端发送数据时,可以调用这个函数将数据对象转化为 JSON 格式的字符串。
JSON.parse() 函数,这个函数用来将 JSON 格式的字符串转换为一个 js 数据结构,如果传入的字符串不是标准的 JSON 格式的字符串的话,将会抛出错误。当从后端接收到 JSON 格式的字符串时,可以通过这个方法来将其解析为一个 js 数据结构,以此来进行数据的访问。
JavaScript 中如何进行隐式类型转换?
首先要介绍ToPrimitive
方法,这是 JavaScript 中每个值隐含的自带的方法,用来将值 (无论是基本类型值还是对象)转换为基本类型值。如果值为基本类型,则直接返回值本身;如果值为对象,其看起来大概是这样:
type
的值为number
或者string
。
(1)当type
为number
时规则如下:
调用
obj
的valueOf
方法,如果为原始值,则返回,否则下一步;调用
obj
的toString
方法,后续同上;抛出
TypeError
异常。
(2)当type
为string
时规则如下:
调用
obj
的toString
方法,如果为原始值,则返回,否则下一步;调用
obj
的valueOf
方法,后续同上;抛出
TypeError
异常。
可以看出两者的主要区别在于调用toString
和valueOf
的先后顺序。默认情况下:
如果对象为 Date 对象,则
type
默认为string
;其他情况下,
type
默认为number
。
总结上面的规则,对于 Date 以外的对象,转换为基本类型的大概规则可以概括为一个函数:
而 JavaScript 中的隐式类型转换主要发生在+、-、*、/
以及==、>、<
这些运算符之间。而这些运算符只能操作基本类型值,所以在进行这些运算前的第一步就是将两边的值用ToPrimitive
转换成基本类型,再进行操作。
以下是基本类型的值在不同操作符的情况下隐式转换的规则 (对于对象,其会被ToPrimitive
转换成基本类型,所以最终还是要应用基本类型转换规则):
+
操作符+
操作符的两边有至少一个string
类型变量时,两边的变量都会被隐式转换为字符串;其他情况下两边的变量都会被转换为数字。
-
、*
、\
操作符
NaN
也是一个数字
对于
==
操作符
操作符两边的值都尽量转成number
:
对于
<
和>
比较符
如果两边都是字符串,则比较字母表顺序:
其他情况下,转换为数字再比较:
以上说的是基本类型的隐式转换,而对象会被ToPrimitive
转换为基本类型再进行转换:
其对比过程如下:
又比如:
运算过程如下:
宏任务和微任务分别有哪些
微任务包括: promise 的回调、node 中的 process.nextTick 、对 Dom 变化监听的 MutationObserver。
宏任务包括: script 脚本的执行、setTimeout ,setInterval ,setImmediate 一类的定时事件,还有如 I/O 操作、UI 渲染等。
let、const、var 的区别
(1)块级作用域: 块作用域由 { }
包括,let 和 const 具有块级作用域,var 不存在块级作用域。块级作用域解决了 ES5 中的两个问题:
内层变量可能覆盖外层变量
用来计数的循环变量泄露为全局变量
(2)变量提升: var 存在变量提升,let 和 const 不存在变量提升,即在变量只能在声明之后使用,否在会报错。
(3)给全局添加属性: 浏览器的全局对象是 window,Node 的全局对象是 global。var 声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是 let 和 const 不会。
(4)重复声明: var 声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const 和 let 不允许重复声明变量。
(5)暂时性死区: 在使用 let、const 命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用 var 声明的变量不存在暂时性死区。
(6)初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而 const 声明变量必须设置初始值。
(7)指针指向: let 和 const 都是 ES6 新增的用于创建变量的语法。 let 创建的变量是可以更改指针指向(可以重新赋值)。但 const 声明的变量是不允许改变指针的指向。
Promise.allSettled
描述:等到所有promise
都返回结果,就返回一个promise
实例。
实现:
如何判断一个对象是否属于某个类?
第一种方式,使用 instanceof 运算符来判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。
第二种方式,通过对象的 constructor 属性来判断,对象的 constructor 属性指向该对象的构造函数,但是这种方式不是很安全,因为 constructor 属性可以被改写。
第三种方式,如果需要判断的是某个内置的引用类型的话,可以使用 Object.prototype.toString() 方法来打印对象的[[Class]] 属性来进行判断。
手写题:数组去重
代码输出结果
输出结果为:
代码执行过程如下:
首先遇到定时器,将其加入到宏任务队列;
遇到 Promise,首先执行里面的同步代码,打印出 2,遇到 resolve,将其加入到微任务队列,执行后面同步代码,打印出 3;
继续执行 script 中的代码,打印出 7 和 8,至此第一轮代码执行完成;
执行微任务队列中的代码,首先打印出 4,如遇到 Promise,执行其中的同步代码,打印出 5,遇到定时器,将其加入到宏任务队列中,此时宏任务队列中有两个定时器;
执行宏任务队列中的代码,这里我们需要注意是的第一个定时器的时间为 100ms,第二个定时器的时间为 10ms,所以先执行第二个定时器,打印出 6;
此时微任务队列为空,继续执行宏任务队列,打印出 1。
做完这道题目,我们就需要格外注意,每个定时器的时间,并不是所有定时器的时间都为 0 哦。
函数防抖
触发高频事件 N 秒后只会执行一次,如果 N 秒内事件再次触发,则会重新计时。
简单版:函数内部支持使用 this 和 event 对象;
使用:
最终版:除了支持 this 和 event 外,还支持以下功能:
支持立即执行;
函数可能有返回值;
支持取消功能;
使用:
Cookie 有哪些字段,作用分别是什么
Cookie 由以下字段组成:
Name:cookie 的名称
Value:cookie 的值,对于认证 cookie,value 值包括 web 服务器所提供的访问令牌;
Size: cookie 的大小
Path:可以访问此 cookie 的页面路径。 比如 domain 是 abc.com,path 是
/test
,那么只有/test
路径下的页面可以读取此 cookie。Secure: 指定是否使用 HTTPS 安全协议发送 Cookie。使用 HTTPS 安全协议,可以保护 Cookie 在浏览器和 Web 服务器间的传输过程中不被窃取和篡改。该方法也可用于 Web 站点的身份鉴别,即在 HTTPS 的连接建立阶段,浏览器会检查 Web 网站的 SSL 证书的有效性。但是基于兼容性的原因(比如有些网站使用自签署的证书)在检测到 SSL 证书无效时,浏览器并不会立即终止用户的连接请求,而是显示安全风险信息,用户仍可以选择继续访问该站点。
Domain:可以访问该 cookie 的域名,Cookie 机制并未遵循严格的同源策略,允许一个子域可以设置或获取其父域的 Cookie。当需要实现单点登录方案时,Cookie 的上述特性非常有用,然而也增加了 Cookie 受攻击的危险,比如攻击者可以借此发动会话定置攻击。因而,浏览器禁止在 Domain 属性中设置.org、.com 等通用顶级域名、以及在国家及地区顶级域下注册的二级域名,以减小攻击发生的范围。
HTTP: 该字段包含
HTTPOnly
属性 ,该属性用来设置 cookie 能否通过脚本来访问,默认为空,即可以通过脚本访问。在客户端是不能通过 js 代码去设置一个 httpOnly 类型的 cookie 的,这种类型的 cookie 只能通过服务端来设置。该属性用于防止客户端脚本通过document.cookie
属性访问 Cookie,有助于保护 Cookie 不被跨站脚本攻击窃取或篡改。但是,HTTPOnly 的应用仍存在局限性,一些浏览器可以阻止客户端脚本对 Cookie 的读操作,但允许写操作;此外大多数浏览器仍允许通过 XMLHTTP 对象读取 HTTP 响应中的 Set-Cookie 头。Expires/Max-size : 此 cookie 的超时时间。若设置其值为一个时间,那么当到达此时间后,此 cookie 失效。不设置的话默认值是 Session,意思是 cookie 会和 session 一起失效。当浏览器关闭(不是浏览器标签页,而是整个浏览器) 后,此 cookie 失效。
总结: 服务器端可以使用 Set-Cookie 的响应头部来配置 cookie 信息。一条 cookie 包括了 5 个属性值 expires、domain、path、secure、HttpOnly。其中 expires 指定了 cookie 失效的时间,domain 是域名、path 是路径,domain 和 path 一起限制了 cookie 能够被哪些 url 访问。secure 规定了 cookie 只能在确保安全的情况下传输,HttpOnly 规定了这个 cookie 只能被服务器访问,不能使用 js 脚本访问。
代码输出结果
输出结果如下:
代码的执行过程如下:
首先进入
async1
,打印出async1 start
;之后遇到
async2
,进入async2
,遇到定时器timer2
,加入宏任务队列,之后打印async2
;由于
async2
阻塞了后面代码的执行,所以执行后面的定时器timer3
,将其加入宏任务队列,之后打印start
;然后执行 async2 后面的代码,打印出
async1 end
,遇到定时器 timer1,将其加入宏任务队列;最后,宏任务队列有三个任务,先后顺序为
timer2
,timer3
,timer1
,没有微任务,所以直接所有的宏任务按照先进先出的原则执行。
为什么 0.1 + 0.2 != 0.3,请详述理由
因为 JS 采用 IEEE 754 双精度版本(64 位),并且只要采用 IEEE 754 的语言都有该问题。
我们都知道计算机表示十进制是采用二进制表示的,所以 0.1
在二进制表示为
那么如何得到这个二进制的呢,我们可以来演算下
小数算二进制和整数不同。乘法计算时,只计算小数位,整数位用作每一位的二进制,并且得到的第一位为最高位。所以我们得出 0.1 = 2^-4 * 1.10011(0011)
,那么 0.2
的演算也基本如上所示,只需要去掉第一步乘法,所以得出 0.2 = 2^-3 * 1.10011(0011)
。
回来继续说 IEEE 754 双精度。六十四位中符号位占一位,整数位占十一位,其余五十二位都为小数位。因为 0.1
和 0.2
都是无限循环的二进制了,所以在小数位末尾处需要判断是否进位(就和十进制的四舍五入一样)。
所以 2^-4 * 1.10011...001
进位后就变成了 2^-4 * 1.10011(0011 * 12次)010
。那么把这两个二进制加起来会得出 2^-2 * 1.0011(0011 * 11次)0100
, 这个值算成十进制就是 0.30000000000000004
下面说一下原生解决办法,如下代码所示
Promise 的基本用法
(1)创建 Promise 对象
Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。
Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。
一般情况下都会使用new Promise()
来创建 promise 对象,但是也可以使用promise.resolve
和promise.reject
这两个方法:
Promise.resolve
Promise.resolve(value)
的返回值也是一个 promise 对象,可以对返回值进行.then 调用,代码如下:
resolve(11)
代码中,会让 promise 对象进入确定(resolve
状态),并将参数11
传递给后面的then
所指定的onFulfilled
函数;
创建 promise 对象可以使用new Promise
的形式创建对象,也可以使用Promise.resolve(value)
的形式创建 promise 对象;
Promise.reject
Promise.reject
也是new Promise
的快捷形式,也创建一个 promise 对象。代码如下:
就是下面的代码 new Promise 的简单形式:
下面是使用 resolve 方法和 reject 方法:
上面的代码的含义是给testPromise
方法传递一个参数,返回一个 promise 对象,如果为true
的话,那么调用 promise 对象中的resolve()
方法,并且把其中的参数传递给后面的then
第一个函数内,因此打印出 “hello world
”, 如果为false
的话,会调用 promise 对象中的reject()
方法,则会进入then
的第二个函数内,会打印No thanks
;
(2)Promise 方法
Promise 有五个常用的方法:then()、catch()、all()、race()、finally。下面就来看一下这些方法。
then()
当 Promise 执行的内容符合成功条件时,调用resolve
函数,失败就调用reject
函数。Promise 创建完了,那该如何调用呢?
then
方法可以接受两个回调函数作为参数。第一个回调函数是 Promise 对象的状态变为resolved
时调用,第二个回调函数是 Promise 对象的状态变为rejected
时调用。其中第二个参数可以省略。 then
方法返回的是一个新的 Promise 实例(不是原来那个 Promise 实例)。因此可以采用链式写法,即then
方法后面再调用另一个 then 方法。
当要写有顺序的异步事件时,需要串行时,可以这样写:
那当要写的事件没有顺序或者关系时,还如何写呢?可以使用all
方法来解决。
2. catch()
Promise 对象除了有 then 方法,还有一个 catch 方法,该方法相当于then
方法的第二个参数,指向reject
的回调函数。不过catch
方法还有一个作用,就是在执行resolve
回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch
方法中。
3. all()
all
方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个promise
对象。当数组中所有的promise
的状态都达到resolved
的时候,all
方法的状态就会变成resolved
,如果有一个状态变成了rejected
,那么all
方法的状态就会变成rejected
。
调用all
方法时的结果成功的时候是回调函数的参数也是一个数组,这个数组按顺序保存着每一个 promise 对象resolve
执行时的值。
(4)race()
race
方法和all
一样,接受的参数是一个每项都是promise
的数组,但是与all
不同的是,当最先执行完的事件执行完之后,就直接返回该promise
对象的值。如果第一个promise
对象状态变成resolved
,那自身的状态变成了resolved
;反之第一个promise
变成rejected
,那自身状态就会变成rejected
。
那么race
方法有什么实际作用呢?当要做一件事,超过多长时间就不做了,可以用这个方法来解决:
5. finally()
finally
方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
上面代码中,不管promise
最后的状态,在执行完then
或catch
指定的回调函数以后,都会执行finally
方法指定的回调函数。
下面是一个例子,服务器使用 Promise 处理请求,然后使用finally
方法关掉服务器。
finally
方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled
还是rejected
。这表明,finally
方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。finally
本质上是then
方法的特例:
上面代码中,如果不使用finally
方法,同样的语句需要为成功和失败两种情况各写一次。有了finally
方法,则只需要写一次。
两栏布局的实现
一般两栏布局指的是左边一栏宽度固定,右边一栏宽度自适应,两栏布局的具体实现:
利用浮动,将左边元素宽度设置为 200px,并且设置向左浮动。将右边元素的 margin-left 设置为 200px,宽度设置为 auto(默认为 auto,撑满整个父元素)。
利用浮动,左侧元素设置固定大小,并左浮动,右侧元素设置 overflow: hidden; 这样右边就触发了 BFC,BFC 的区域不会与浮动元素发生重叠,所以两侧就不会发生重叠。
利用 flex 布局,将左边元素设置为固定宽度 200px,将右边的元素设置为 flex:1。
利用绝对定位,将父级元素设置为相对定位。左边元素设置为 absolute 定位,并且宽度设置为 200px。将右边元素的 margin-left 的值设置为 200px。
利用绝对定位,将父级元素设置为相对定位。左边元素宽度设置为 200px,右边元素设置为绝对定位,左边定位为 200px,其余方向定位为 0。
深/浅拷贝
首先判断数据类型是否为对象,如果是对象(数组|对象),则递归(深/浅拷贝),否则直接拷贝。
这个函数只能判断 obj
是否是对象,无法判断其具体是数组还是对象。
如果 new 一个箭头函数的会怎么样
箭头函数是 ES6 中的提出来的,它没有 prototype,也没有自己的 this 指向,更不可以使用 arguments 参数,所以不能 New 一个箭头函数。
new 操作符的实现步骤如下:
创建一个对象
将构造函数的作用域赋给新对象(也就是将对象的__proto__属性指向构造函数的 prototype 属性)
指向构造函数中的代码,构造函数中的 this 指向该对象(也就是为这个对象添加属性和方法)
返回新的对象
所以,上面的第二、三步,箭头函数都是没有办法执行的。
Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题 ?你能说说如下代码的实现原理么?
1)Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题
Vue 使用了 Object.defineProperty 实现双向数据绑定
在初始化实例时对属性执行 getter/setter 转化
属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的(这也就造成了 Vue 无法检测到对象属性的添加或删除)
所以 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)
2)接下来我们看看框架本身是如何实现的呢?
Vue 源码位置:vue/src/core/instance/index.js
我们阅读以上源码可知,vm.$set 的实现原理是:
如果目标是数组,直接使用数组的 splice 方法触发相应式;
如果目标是对象,会先判读属性是否存在、对象是否是响应式,
最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理
defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法
评论