《前端100问》50

41、下面代码输出什么

var a = 10;
(function () {
    console.log(a)//undefined
    a = 5
    console.log(window.a)//10
    var a = 20;
    console.log(a)//20
})()

分别为 undefined 10 20,原因是作用域问题

在内部声名var a = 20;相当于先声明 var a 然后再执行赋值操作,这是在 IIFE 内形成的独立作用域。

如果把 var a=20注释掉,那么 a 只有在外部有声明,显示的就是外部的 a 变量的值了。结果 a 会是 10 5 5

42、实现一个 sleep 函数

比如 sleep(1000) 意味着等待1000毫秒,可从 Promise、Generator、Async/Await 等角度实现

// Promise 实现
const sleep = time => new Promise(resolve => setTimeout(resolve, time))

sleep(1000).then(() => {
  console.log(1)
})

// Generator
function* sleepGenerator(time) {
  yield new Promise(resolve => setTimeout(resolve, time))
}
//next() 返回一个由 yield表达式生成的值。value拿到返回值
sleepGenerator(1000)
  .next()
  .value.then(() => {
    console.log(1)
  })

//async
const sleep = time => new Promise(resolve => setTimeout(resolve, time))
async function output() {
  await sleep(1000)
  console.log(1)
}
output()

// ES5 回调函数
function sleep(callback, time) {
  setTimeout(callback, time)
}
sleep(function output() {
  console.log(1)
}, 1000)

43、使用 sort() 进行排序,输出结果

使用 sort() 对数组 [3, 15, 8, 29, 102, 22] 进行排序,输出结果

[3, 15, 8, 29, 102, 22].sort() // [ 102, 15, 22, 29, 3, 8 ]

根据MDN上对Array.sort()的解释,默认的排序方法会将数组元素转换为字符串,然后比较字符串中字符的UTF-16编码顺序来进行排序。所以'102' 会排在 '15' 前面。

Array.prototype.sort()

sort() 方法用原地算法对数组的元素进行排序,并返回数组。默认排序顺序是在将元素转换为字符串,然后比较它们的UTF-16代码单元值序列时构建的

返回值:

排序后的数组。请注意,数组已原地排序,并且不进行复制。

[3, 15, 8, 29, 102, 22].sort(function(a,b) {
return a-b;
})
//输出: [3, 8, 15, 22, 29, 102]

44、介绍 HTTPS 握手过程

一、Client → Server

Client Hello

握手开始时,总是由先客户端会发送 Client Hello 信息给服务端,主要包含

  • Version Number 客户端支持的协议版本
  • Randomly Generated Data 32 字节长度的随机值,用于之后生成主密钥。
  • Session Identification Session ID,第一次连接时为空。
  • Cipher Suite 客户端支持的加密算法列表,按优先级顺序排列。

二、Server → Client

Server Hello

接着,服务端收到客户端发来的消息之后,会返回 Server Hello 信息给客户端,告知客户端接下来使用的一些参数

  • Version Number 通信协议版本
  • Randomly Generated Data 32 字节长度的随机值,用于之后生成主密钥
  • Session Identification Session ID
  • Cipher Suite 加密算法

Server Certificate 证书

服务端还会带上证书返回给客户端。证书中含有服务端的公钥、网站地址、证书的颁发机构等信息。

客户端收到服务端返回的证书之后,会验证该证书的真实合法性。

Server Key Exchange 额外数据

这个是可选的,取决于使用的加密算法。主要是携带密钥交换的额外数据。

Server Hello Done

表示服务端已经发送完毕,并等待客户端回应。

三、Client → Server

Client Key Exchange

客户端使用之前发送给服务端及服务端返回的随机数,生成预主密钥,然后用服务端返回的公钥进行加密。

Change Cipher Spec

告诉服务端,之后的所有信息都会使用协商好的密钥和算法加密

Client Finished

客户端的握手工作已经完成。这条信息是握手过程中所有消息的散列值。

四、Server → Client

Change Cipher Spec Message

告知客户端,会使用刚刚协商的密钥来加密信息

Server Finished Message

表示服务端的握手工作已经完成

数字证书申请流程

  1. 网站提交身份信息给CA机构
  2. CA机构审核信息的真实性
  3. 对信息进行Hash,计算信息摘要
  4. CA机构的密钥加密信息摘要,得到数字签名

浏览器验证证书

  1. 浏览器利用证书的原始信息计算得到信息摘要
  2. 利用CA的公钥解密数字证书中的数字签名,解密出来的数据也是信息摘要
  3. 比较两个摘要是否相等

45、HTTPS 握手过程中,客户端如何验证证书的合法性

  1. 首先浏览器读取证书中的证书所有者、有效期等信息进行校验,校验证书的网站域名是否与证书颁发的域名一致,校验证书是否在有效期内

  2. 浏览器开始查找操作系统中已内置的受信任的证书发布机构CA,与服务器发来的证书中的颁发者CA比对,用于校验证书是否为合法机构颁发

  3. 两种情况:

    1. 如果找不到,浏览器就会报错,说明服务器发来的证书是不可信任的。

    2. 如果找到,那么浏览器就会从操作系统中取出颁发者CA 的公钥(多数浏览器开发商发布
      版本时,会事先在内部植入常用认证机关的公开密钥),然后对服务器发来的证书里面的签名进行解密

  4. 浏览器使用相同的hash算法计算出服务器发来的证书的hash值,将这个计算的hash值与证书中签名做对比

  5. 对比结果一致,则证明服务器发来的证书合法,没有被冒充

46、输出以下代码执行的结果并解释为什么

var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)

MDN描述:

push方法将值追加到数组中。

push 方法具有通用性。该方法和 call()apply() 一起使用时,可应用在类似数组的对象上。

push 方法根据 length 属性来决定从哪里开始插入给定的值。

如果 length 不能被转成一个数值,则插入的元素索引为 0,包括 length 不存在时。

length 不存在时,将会创建它。

var obj = {
    '2': 3,
    '3': 4,
    'length': 2,    
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)
//打印: {2: 1, 3: 2, length: 4, push: ƒ}
var obj1 = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj1.push(1)
obj1.push(2)
console.log(obj1)
//Object(4) [empty × 2, 1, 2, splice: ƒ, push: ƒ]

var obj2 = {
  2: 3,
  3: 4,
  push: Array.prototype.push
}
obj2.push(1)
obj2.push(2)
console.log(obj2)
// {0: 1, 1: 2, 2: 3, 3: 4, length: 2, push: ƒ}

解释:

push 方法具有通用性,所以可以给类数组使用。具体是根据类数组的 length 属性,将其转换为数值当做对象[数值]来使用,而对象使用对象[数值]时,数值一律当成字符处理也是就是obj[1]obj['1']是等价的。

所以push会解析 length的值 往obj[2] push 一个 1 ,所以obj 的属性 2 的值会变成 1,此时 obj 并没有数组的独有方法,所以JS解析器还是会打印一个对象。

obj1有一个只有数组的才有的 splice 方法,而且 length4 所以 JS解析器会将其当成数组来打印输出,从下标0开始读数据,但是并没有数据会补空最后输出。

47、双向绑定和 vuex 是否冲突

在严格模式下直接使用确实会有问题。

在严格模式中使用Vuex,当用户输入时,v-model 会试图直接修改属性值,但这个修改不是在 mutation中修改的,所以会抛出一个错误。

处理方法:

来自官网文档:

当在严格模式中使用 Vuex 时,在属于 Vuex 的 state 上使用 v-model 会比较棘手:

<input v-model="obj.message">

假设这里的 obj 是在计算属性中返回的一个属于 Vuex store 的对象,在用户输入时,v-model 会试图直接修改 obj.message。在严格模式中,由于这个修改不是在 mutation 函数中执行的, 这里会抛出一个错误。

方法一:不用v-model 直接绑定value值和监听input事件

用“Vuex 的思维”去解决这个问题的方法是:给 <input> 中绑定 value,然后侦听 input 或者 change 事件,在事件回调中调用一个方法:

<input :value="message" @input="updateMessage">
// ...
computed: {
  ...mapState({
    message: state => state.obj.message
  })
},
methods: {
  updateMessage (e) {
    this.$store.commit('updateMessage', e.target.value)
  }
}

下面是 mutation 函数:

// ...
mutations: {
  updateMessage (state, message) {
    state.obj.message = message
  }
}

方法二:双向绑定的计算属性 给计算属性设置gettersetter

必须承认,这样做比简单地使用“v-model + 局部状态”要啰嗦得多,并且也损失了一些 v-model 中很有用的特性。另一个方法是使用带有 setter 的双向绑定计算属性:

<input v-model="message">
// ...
computed: {
  message: {
    get () {
      return this.$store.state.obj.message //读 Vuex 数据
    },
    set (value) {
      this.$store.commit('updateMessage', value)//提交 mutation 更改 Vuex 数据
    }
  }
}

48、call 和 apply 的区别是什么,哪个性能更好一些

  1. Function.prototype.applyFunction.prototype.call 的作用是一样的,区别在于传入参数的不同;
  2. 第一个参数都是,指定函数体内 this 的指向;
  3. 第二个参数开始不同,apply 是传入带下标的集合数组或者类数组apply把它传给函数作为参数,call从第二个开始传入的参数是不固定的,都会传给函数作为参数。
  4. callapply 的性能要好,平常可以多用 call,call 传入参数的格式正是内部所需要的格式。

49、为什么通常在发送数据埋点请求的时候使用的是 1x1 像素的透明 gif 图片?

前端监控的原理

所谓的前端监控,其实是在满足一定条件后,由Web页面将用户信息(UA/鼠标点击位置/页面报错/停留时长/etc)上报给服务器的过程。一般是将上报数据用url_encode(百度统计/CNZZ)或JSON编码(神策/诸葛io)为字符串,通过url参数传递给服务器,然后在服务器端统一处理。

这套流程的关键在于:

1)能够收集到用户信息;

2)能够将收集到的数据上报给服务器。也就是说,只要能上报数据,无论是请求GIF文件还是请求js文件或者是调用页面接口,服务器端其实并不关心具体的上报方式。

首先,为什么不能直接用GET/POST/HEAD请求接口进行上报?

一般而言,打点域名都不是当前域名,所以所有的接口请求都会构成跨域。而跨域请求很容易出现由于配置不当被浏览器拦截并报错,这是不能接受的。所以,直接排除。

为什么不能用请求其他的文件资源(js/css/ttf)的方式进行上报?

这和浏览器的特性有关。通常,创建资源节点后只有将对象注入到浏览器DOM树后,浏览器才会实际发送资源请求。反复操作DOM不仅会引发性能问题,而且载入js/css资源还会阻塞页面渲染,影响用户体验。

但是图片请求例外。构造图片打点不仅不用插入DOM,只要在 JS 中 new 出 Image 对象就能发起请求,而且还没有阻塞问题,在没有 JS 的浏览器环境中也能通过 img标签正常打点,这是其他类型的资源请求所做不到的。

同样都是图片,上报时选用了1x1的透明GIF,而不是其他的PNG/JEPG/BMP文件?

首先,1x1像素是最小的合法图片。

而且,因为是通过图片打点,所以图片最好是透明的,这样一来不会影响页面本身展示效果,二者表示图片透明只要使用一个二进制位标记图片是透明色即可,不用存储色彩空间数据,可以节约体积。

因为需要透明色,所以可以直接排除JEPG(BMP32格式可以支持透明色)。

在体积方面最小的BMP文件需要74个字节,PNG需要67个字节,而合法的GIF,只需要43个字节。

同样的响应,GIF可以比BMP节约41%的流量,比PNG节约35%的流量。

所以,总结:

前端监控使用GIF进行上报主要是因为:

  • 没有跨域问题;
  • 能够完成整个 HTTP 请求+响应(尽管不需要响应内容)
  • 触发 GET 请求之后不需要获取和处理数据、服务器也不需要发送数据
  • 不会阻塞页面加载,影响用户体验
  • 相比 XMLHttpRequest 对象发送 GET 请求,性能上更好
  • 在所有图片中体积最小,相较BMP/PNG,可以节约41%/35%的网络资源。

50、实现 (5).add(3).minus(2) 功能。

JavaScript 的 Number 对象是经过封装的能让你处理数字值的对象。Number 对象由 Number() 构造器创建。

(5)Number(5)完全等价

Number.isNaN() 确定传递的值是否是 NaN。

Number.prototype.valueOf() valueOf() 方法返回一个被 Number对象包装的原始值。

Number.prototype.add = function (n) {
  if (typeof n !== 'number') throw new Error('请输入数字~')
  return this.valueOf() + n
}
Number.prototype.minus = function (n) {
  if (typeof n !== 'number') throw new Error('请输入数字~')
  return this.valueOf() - n
}

console.log((5).add(3).minus(2))
Number.prototype.add = function (value) {
  let number = parseFloat(value)
  if (typeof number !== 'number' || Number.isNaN(number)) {
    throw new Error('请输入数字或者数字字符串~')
  }
  return this + number
}
Number.prototype.minus = function (value) {
  let number = parseFloat(value)
  if (typeof number !== 'number' || Number.isNaN(number)) {
    throw new Error('请输入数字或者数字字符串~')
  }
  return this - number
}
console.log((5).add(3).minus(2))
//更加强壮,排除了NaN 兼容了字符串

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!