TypechoJoeTheme

一生亦木

标签搜索

前端面试高频考点

2021-06-15
/
1 评论
/
78 阅读
/
正在检测是否收录...
06/15

高频考点

[toc]

1 typeof类型判断

typeof 是否能正确判断类型?instanceof 能正确判断对象的原理是什么

  • typeof 对于原始类型来说,除了 null 都可以显示正确的类型
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'

typeof 对于对象来说,除了函数都会显示 object,所以说 typeof 并不能准确判断变量到底是什么类型

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'

如果我们想判断一个对象的正确类型,这时候可以考虑使用 instanceof,因为内部机制是通过原型链来判断的

const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true

var str = 'hello world'
str instanceof String // false

var str1 = new String('hello world')
str1 instanceof String // true

对于原始类型来说,你想直接通过 instanceof来判断类型是不行的

2 类型转换

首先我们要知道,在 JS 中类型转换只有三种情况,分别是:

  • 转换为布尔值
  • 转换为数字
  • 转换为字符串

转Boolean

在条件判断时,除了 undefinednullfalseNaN''0-0,其他所有值都转为 true,包括所有对象

对象转原始类型

对象在转换类型的时候,会调用内置的 [[ToPrimitive]] 函数,对于该函数来说,算法逻辑一般来说如下

  • 如果已经是原始类型了,那就不需要转换了
  • 调用 x.valueOf(),如果转换为基础类型,就返回转换的值
  • 调用 x.toString(),如果转换为基础类型,就返回转换的值
  • 如果都没有返回原始类型,就会报错

当然你也可以重写 Symbol.toPrimitive,该方法在转原始类型时调用优先级最高。

let a = {
  valueOf() {
    return 0
  },
  toString() {
    return '1'
  },
  [Symbol.toPrimitive]() {
    return 2
  }
}
1 + a // => 3

四则运算符

它有以下几个特点:

  • 运算中其中一方为字符串,那么就会把另一方也转换为字符串
  • 如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"
  • 对于第一行代码来说,触发特点一,所以将数字 1 转换为字符串,得到结果 '11'
  • 对于第二行代码来说,触发特点二,所以将 true 转为数字 1
  • 对于第三行代码来说,触发特点二,所以将数组通过 toString转为字符串 1,2,3,得到结果 41,2,3

另外对于加法还需要注意这个表达式 'a' + + 'b'

'a' + + 'b' // -> "aNaN"
  • 因为 + 'b' 等于 NaN,所以结果为 "aNaN",你可能也会在一些代码中看到过 + '1'的形式来快速获取 number 类型。
  • 那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字
4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN

比较运算符

  • 如果是对象,就通过 toPrimitive 转换对象
  • 如果是字符串,就通过 unicode 字符索引来比较
let a = {
  valueOf() {
    return 0
  },
  toString() {
    return '1'
  }
}
a > -1 // true

在以上代码中,因为 a 是对象,所以会通过 valueOf 转换为原始类型再比较值。

3 This

我们先来看几个函数调用的场景

function foo() {
  console.log(this.a)
}
var a = 1
foo()

const obj = {
  a: 2,
  foo: foo
}
obj.foo()

const c = new foo()
  • 对于直接调用 foo 来说,不管 foo 函数被放在了什么地方,this 一定是window
  • 对于 obj.foo() 来说,我们只需要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo 函数中的 this 就是 obj 对象
  • 对于 new 的方式来说,this 被永远绑定在了 c 上面,不会被任何方式改变 this

说完了以上几种情况,其实很多代码中的 this 应该就没什么问题了,下面让我们看看箭头函数中的 this

function a() {
  return () => {
    return () => {
      console.log(this)
    }
  }
}
console.log(a()()())
  • 首先箭头函数其实是没有 this 的,箭头函数中的 this 只取决包裹箭头函数的第一个普通函数的 this。在这个例子中,因为包裹箭头函数的第一个普通函数是 a,所以此时的 thiswindow。另外对箭头函数使用 bind这类函数是无效的。
  • 最后种情况也就是 bind 这些改变上下文的 API 了,对于这些函数来说,this 取决于第一个参数,如果第一个参数为空,那么就是 window
  • 那么说到 bind,不知道大家是否考虑过,如果对一个函数进行多次 bind,那么上下文会是什么呢?
let a = {}
let fn = function () { console.log(this) }
fn.bind().bind(a)() // => ?

如果你认为输出结果是 a,那么你就错了,其实我们可以把上述代码转换成另一种形式

// fn.bind().bind(a) 等于
let fn2 = function fn1() {
  return function() {
    return fn.apply()
  }.apply(a)
}
fn2()

可以从上述代码中发现,不管我们给函数 bind 几次,fn 中的 this 永远由第一次 bind 决定,所以结果永远是 window

let a = { name: 'poetries' }
function foo() {
  console.log(this.name)
}
foo.bind(a)() // => 'poetries'

以上就是 this 的规则了,但是可能会发生多个规则同时出现的情况,这时候不同的规则之间会根据优先级最高的来决定 this 最终指向哪里。

首先,new 的方式优先级最高,接下来是 bind 这些函数,然后是 obj.foo() 这种调用方式,最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。

4 == 和 === 有什么区别

对于 == 来说,如果对比双方的类型不一样的话,就会进行类型转换

假如我们需要对比 x 和 y 是否相同,就会进行如下判断流程

  1. 首先会判断两者类型是否相同。相同的话就是比大小了
  2. 类型不相同的话,那么就会进行类型转换
  3. 会先判断是否在对比 nullundefined,是的话就会返回 true
  4. 判断两者类型是否为 stringnumber,是的话就会将字符串转换为 number
1 == '1'
      ↓
1 ==  1
  1. 判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断
'1' == true
        ↓
'1' ==  1
        ↓
 1  ==  1
  1. 判断其中一方是否为 object 且另一方为 stringnumber 或者 symbol,是的话就会把 object 转为原始类型再进行判断
'1' == { name: 'yck' }
        ↓
'1' == '[object Object]'

对于 === 来说就简单多了,就是判断两者类型和值是否相同

5 闭包

闭包的定义其实很简单:函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包

function A() {
  let a = 1
  window.B = function () {
      console.log(a)
  }
}
A()
B() // 1

闭包存在的意义就是让我们可以间接访问函数内部的变量

经典面试题,循环中使用闭包解决 var 定义函数的问题

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

首先因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i就是 6 了,所以会输出一堆 6

解决办法有三种

  1. 第一种是使用闭包的方式
for (var i = 1; i <= 5; i++) {
  ;(function(j) {
    setTimeout(function timer() {
      console.log(j)
    }, j * 1000)
  })(i)
}

在上述代码中,我们首先使用了立即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j 上面不会改变,当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的

  1. 第二种就是使用 setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入
for (var i = 1; i <= 5; i++) {
  setTimeout(
    function timer(j) {
      console.log(j)
    },
    i * 1000,
    i
  )
}
  1. 第三种就是使用 let 定义 i 了来解决问题了,这个也是最为推荐的方式
for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

6 深浅拷贝

浅拷贝

首先可以通过 Object.assign 来解决这个问题,很多人认为这个函数是用来深拷贝的。其实并不是,Object.assign 只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝

let a = {
  age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1

另外我们还可以通过展开运算符 ... 来实现浅拷贝

let a = {
  age: 1
}
let b = { ...a }
a.age = 2
console.log(b.age) // 1

通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就可能需要使用到深拷贝了

let a = {
  age: 1,
  jobs: {
    first: 'FE'
  }
}
let b = { ...a }
a.jobs.first = 'native'
console.log(b.jobs.first) // native

浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到最开始的话题了,两者享有相同的地址。要解决这个问题,我们就得使用深拷贝了。

深拷贝

这个问题通常可以通过 JSON.parse(JSON.stringify(object)) 来解决。

let a = {
  age: 1,
  jobs: {
    first: 'FE'
  }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE

但是该方法也是有局限性的

  • 会忽略 undefined
  • 会忽略 symbol
  • 不能序列化函数
  • 不能解决循环引用的对象
let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  },
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)

更多详情 https://www.jianshu.com/p/2d8a26b3958f

7 原型

原型链就是多个对象通过 __proto__ 的方式连接了起来。为什么 obj 可以访问到 valueOf 函数,就是因为 obj 通过原型链找到了 valueOf 函数

.png)

  • Object 是所有对象的爸爸,所有对象都可以通过 __proto__找到它
  • Function 是所有函数的爸爸,所有函数都可以通过 __proto__ 找到它
  • 函数的 prototype 是一个对象
  • 对象的__proto__ 属性指向原型, __proto__ 将对象和原型连接起来组成了原型链

8 var、let 及 const 区别

涉及面试题:什么是提升?什么是暂时性死区?var、let 及 const 区别?

  • 函数提升优先于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部
  • var 存在提升,我们能在声明之前使用。letconst 因为暂时性死区的原因,不能在声明前使用
  • var 在全局作用域下声明变量会导致变量挂载在 window上,其他两者不会
  • letconst 作用基本一致,但是后者声明的变量不能再次赋值

9 原型继承和 Class 继承

涉及面试题:原型如何实现继承?Class 如何实现继承?Class 本质是什么?

首先先来讲下 class,其实在 JS中并不存在类,class 只是语法糖,本质还是函数

class Person {}
Person instanceof Function // true

组合继承

组合继承是最常用的继承方式

function Parent(value) {
  this.val = value
}
Parent.prototype.getValue = function() {
  console.log(this.val)
}
function Child(value) {
  Parent.call(this, value)
}
Child.prototype = new Parent()

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true
  • 以上继承的方式核心是在子类的构造函数中通过 Parent.call(this) 继承父类的属性,然后改变子类的原型为 new Parent() 来继承父类的函数。
  • 这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费

寄生组合继承

这种继承方式对组合继承进行了优化,组合继承缺点在于继承父类函数时调用了构造函数,我们只需要优化掉这点就行了

function Parent(value) {
  this.val = value
}
Parent.prototype.getValue = function() {
  console.log(this.val)
}

function Child(value) {
  Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype, {
  constructor: {
    value: Child,
    enumerable: false,
    writable: true,
    configurable: true
  }
})

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true

以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。

Class 继承

以上两种继承方式都是通过原型去解决的,在 ES6 中,我们可以使用 class 去实现继承,并且实现起来很简单

class Parent {
  constructor(value) {
    this.val = value
  }
  getValue() {
    console.log(this.val)
  }
}
class Child extends Parent {
  constructor(value) {
    super(value)
    this.val = value
  }
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true

class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)

10 模块化

涉及面试题:为什么要使用模块化?都有哪几种方式可以实现模块化,各有什么特点?

使用一个技术肯定是有原因的,那么使用模块化可以给我们带来以下好处

  • 解决命名冲突
  • 提供复用性
  • 提高代码可维护性

立即执行函数

在早期,使用立即执行函数实现模块化是常见的手段,通过函数作用域解决了命名冲突、污染全局作用域的问题

(function(globalVariable){
   globalVariable.test = function() {}
   // ... 声明各种变量、函数都不会污染全局作用域
})(globalVariable)

AMD 和 CMD

鉴于目前这两种实现方式已经很少见到,所以不再对具体特性细聊,只需要了解这两者是如何使用的。

// AMD
define(['./a', './b'], function(a, b) {
  // 加载模块完毕可以使用
  a.do()
  b.do()
})
// CMD
define(function(require, exports, module) {
  // 加载模块
  // 可以把 require 写在函数体的任意地方实现延迟加载
  var a = require('./a')
  a.doSomething()
})

CommonJS

CommonJS 最早是 Node 在使用,目前也仍然广泛使用,比如在 Webpack 中你就能见到它,当然目前在 Node 中的模块管理已经和 CommonJS有一些区别了

// a.js
module.exports = {
    a: 1
}
// or
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1
ar module = require('./a.js')
module.a
// 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {
    a: 1
}
// module 基本实现
var module = {
  id: 'xxxx', // 我总得知道怎么去找到他吧
  exports: {} // exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法相似的原因
var exports = module.exports
var load = function (module) {
    // 导出的东西
    var a = 1
    module.exports = a
    return module.exports
};
// 然后当我 require 的时候去找到独特的
// id,然后将要使用的东西用立即执行函数包装下,over

另外虽然 exportsmodule.exports 用法相似,但是不能对 exports 直接赋值。因为 var exports = module.exports 这句代码表明了 exportsmodule.exports享有相同地址,通过改变对象的属性值会对两者都起效,但是如果直接对 exports 赋值就会导致两者不再指向同一个内存地址,修改并不会对 module.exports 起效

ES Module

ES Module 是原生实现的模块化方案,与 CommonJS 有以下几个区别

  1. CommonJS 支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
  2. CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
  3. CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
  4. ES Module 会编译成 require/exports来执行的
// 引入模块 API
import XXX from './a.js'
import { XXX } from './a.js'
// 导出模块 API
export function a() {}
export default function() {}

11 实现一个简洁版的promise

// 三个常量用于表示状态
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'

function MyPromise(fn) {
    const that = this
    this.state = PENDING

    // value 变量用于保存 resolve 或者 reject 中传入的值
    this.value = null

    // 用于保存 then 中的回调,因为当执行完 Promise 时状态可能还是等待中,这时候应该把 then 中的回调保存起来用于状态改变时使用
    that.resolvedCallbacks = []
    that.rejectedCallbacks = []


    function resolve(value) {
         // 首先两个函数都得判断当前状态是否为等待中
        if(that.state === PENDING) {
            that.state = RESOLVED
            that.value = value

            // 遍历回调数组并执行
            that.resolvedCallbacks.map(cb=>cb(that.value))
        }
    }
    function reject(value) {
        if(that.state === PENDING) {
            that.state = REJECTED
            that.value = value
            that.rejectedCallbacks.map(cb=>cb(that.value))
        }
    }

    // 完成以上两个函数以后,我们就该实现如何执行 Promise 中传入的函数了
    try {
        fn(resolve,reject)
    }cach(e){
        reject(e)
    }
}

// 最后我们来实现较为复杂的 then 函数
MyPromise.prototype.then = function(onFulfilled,onRejected){
  const that = this

  // 判断两个参数是否为函数类型,因为这两个参数是可选参数
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v=>v
  onRejected = typeof onRejected === 'function' ? onRejected : e=>throw e

  // 当状态不是等待态时,就去执行相对应的函数。如果状态是等待态的话,就往回调函数中 push 函数
  if(this.state === PENDING) {
      this.resolvedCallbacks.push(onFulfilled)
      this.rejectedCallbacks.push(onRejected)
  }
  if(this.state === RESOLVED) {
      onFulfilled(that.value)
  }
  if(this.state === REJECTED) {
      onRejected(that.value)
  }
}

12 Event Loop

12.1 进程与线程

涉及面试题:进程与线程区别?JS 单线程带来的好处?

  • JS 是单线程执行的,但是你是否疑惑过什么是线程?
  • 讲到线程,那么肯定也得说一下进程。本质上来说,两个名词都是 CPU 工作时间片的一个描述。
  • 进程描述了 CPU 在运行指令及加载和保存上下文所需的时间,放在应用上来说就代表了一个程序。线程是进程中的更小单位,描述了执行一段指令所需的时间

把这些概念拿到浏览器中来说,当你打开一个 Tab 页时,其实就是创建了一个进程,一个进程中可以有多个线程,比如渲染线程、JS 引擎线程、HTTP 请求线程等等。当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁

  • 上文说到了 JS 引擎线程和渲染线程,大家应该都知道,在 JS 运行的时候可能会阻止 UI 渲染,这说明了两个线程是互斥的。这其中的原因是因为 JS 可以修改 DOM,如果在 JS 执行的时候 UI 线程还在工作,就可能导致不能安全的渲染 UI。这其实也是一个单线程的好处,得益于 JS 是单线程运行的,可以达到节省内存,节约上下文切换时间,没有锁的问题的好处

12.2 执行栈

涉及面试题:什么是执行栈?

可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则

当开始执行 JS 代码时,首先会执行一个 main 函数,然后执行我们的代码。根据先进后出的原则,后执行的函数会先弹出栈,在图中我们也可以发现,foo 函数后执行,当执行完毕后就从栈中弹出了

在开发中,大家也可以在报错中找到执行栈的痕迹

function foo() {
  throw new Error('error')
}
function bar() {
  foo()
}
bar()

.png)

大家可以在上图清晰的看到报错在 foo 函数,foo 函数又是在 bar 函数中调用的

当我们使用递归的时候,因为栈可存放的函数是有限制的,一旦存放了过多的函数且没有得到释放的话,就会出现爆栈的问题

function bar() {
  bar()
}
bar()

.png)

12.3 浏览器中的 Event Loop

涉及面试题:异步代码执行顺序?解释一下什么是 Event Loop

众所周知 JS 是门非阻塞单线程语言,因为在最初 JS 就是为了和浏览器交互而诞生的。如果 JS 是门多线程的语言话,我们在多个线程中处理 DOM 就可能会发生问题(一个线程中新加节点,另一个线程中删除节点)

  • JS 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

console.log('script end');

不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobsmacrotask 称为 task

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

new Promise((resolve) => {
    console.log('Promise')
    resolve()
}).then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');
// script start => Promise => script end => promise1 => promise2 => setTimeout

以上代码虽然 setTimeout 写在 Promise 之前,但是因为 Promise 属于微任务而 setTimeout 属于宏任务

微任务

  • process.nextTick
  • promise
  • Object.observe
  • MutationObserver

宏任务

  • script
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI rendering

宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务

所以正确的一次 Event loop 顺序是这样的

  • 执行同步代码,这属于宏任务
  • 执行栈为空,查询是否有微任务需要执行
  • 执行所有微任务
  • 必要的话渲染 UI
  • 然后开始下一轮 Event loop,执行宏任务中的异步代码

通过上述的 Event loop 顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作 DOM 的话,为了更快的响应界面响应,我们可以把操作 DOM 放入微任务中

12.4 Node 中的 Event loop

  • Node 中的 Event loop 和浏览器中的不相同。
  • NodeEvent loop 分为6个阶段,它们会按照顺序反复运行
┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<──connections───     │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

timer

  • timers 阶段会执行 setTimeoutsetInterval
  • 一个 timer 指定的时间并不是准确时间,而是在达到这个时间后尽快执行回调,可能会因为系统正在执行别的事务而延迟

I/O

  • I/O 阶段会执行除了 close 事件,定时器和 setImmediate 的回调

poll

  • poll 阶段很重要,这一阶段中,系统会做两件事情

    • 执行到点的定时器
    • 执行 poll 队列中的事件
  • 并且当 poll 中没有定时器的情况下,会发现以下两件事情

    • 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者系统限制
    • 如果 poll 队列为空,会有两件事发生
    • 如果有 setImmediate 需要执行,poll 阶段会停止并且进入到 check 阶段执行 setImmediate
    • 如果没有 setImmediate 需要执行,会等待回调被加入到队列中并立即执行回调
    • 如果有别的定时器需要被执行,会回到 timer 阶段执行回调。

check

  • check 阶段执行 setImmediate

close callbacks

  • close callbacks 阶段执行 close 事件
  • 并且在 Node 中,有些情况下的定时器执行顺序是随机的
setTimeout(() => {
    console.log('setTimeout');
}, 0);
setImmediate(() => {
    console.log('setImmediate');
})
// 这里可能会输出 setTimeout,setImmediate
// 可能也会相反的输出,这取决于性能
// 因为可能进入 event loop 用了不到 1 毫秒,这时候会执行 setImmediate
// 否则会执行 setTimeout

上面介绍的都是 macrotask 的执行情况,microtask 会在以上每个阶段完成后立即执行

setTimeout(()=>{
    console.log('timer1')

    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)

setTimeout(()=>{
    console.log('timer2')

    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)

// 以上代码在浏览器和 node 中打印情况是不同的
// 浏览器中一定打印 timer1, promise1, timer2, promise2
// node 中可能打印 timer1, timer2, promise1, promise2
// 也可能打印 timer1, promise1, timer2, promise2

Node 中的 process.nextTick 会先于其他 microtask 执行

setTimeout(() => {
 console.log("timer1");

 Promise.resolve().then(function() {
   console.log("promise1");
 });
}, 0);

process.nextTick(() => {
 console.log("nextTick");
});
// nextTick, timer1, promise1

对于 microtask 来说,它会在以上每个阶段完成前清空 microtask 队列,下图中的 Tick 就代表了 microtask

13 手写 call、apply 及 bind 函数

首先从以下几点来考虑如何实现这几个函数

  • 不传入第一个参数,那么上下文默认为 window
  • 改变了 this 指向,让新的对象可以执行该函数,并能接受参数

实现 call

  • 首先 context为可选参数,如果不传的话默认上下文为 window
  • 接下来给 context 创建一个 fn 属性,并将值设置为需要调用的函数
  • 因为 call 可以传入多个参数作为调用函数的参数,所以需要将参数剥离出来
  • 然后调用函数并将对象上的函数删除
Function.prototype.myCall = function(context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  context = context || window
  context.fn = this
  const args = [...arguments].slice(1)
  const result = context.fn(...args)
  delete context.fn
  return result
}

apply实现

apply 的实现也类似,区别在于对参数的处理

Function.prototype.myApply = function(context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  context = context || window
  context.fn = this
  let result
  // 处理参数和 call 有区别
  if (arguments[1]) {
    result = context.fn(...arguments[1])
  } else {
    result = context.fn()
  }
  delete context.fn
  return result
}

bind 的实现

bind 的实现对比其他两个函数略微地复杂了一点,因为 bind 需要返回一个函数,需要判断一些边界问题,以下是 bind 的实现

  • bind 返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过 new 的方式,我们先来说直接调用的方式
  • 对于直接调用来说,这里选择了 apply 的方式实现,但是对于参数需要注意以下情况:因为 bind 可以实现类似这样的代码 f.bind(obj, 1)(2),所以我们需要将两边的参数拼接起来,于是就有了这样的实现 args.concat(...arguments)
  • 最后来说通过 new 的方式,在之前的章节中我们学习过如何判断 this,对于 new 的情况来说,不会被任何方式改变 this,所以对于这种情况我们需要忽略传入的 this
Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  const _this = this
  const args = [...arguments].slice(1)
  // 返回一个函数
  return function F() {
    // 因为返回了一个函数,我们可以 new F(),所以需要判断
    if (this instanceof F) {
      return new _this(...args, ...arguments)
    }
    return _this.apply(context, args.concat(...arguments))
  }
}

14 new

涉及面试题:new 的原理是什么?通过 new的方式创建对象和通过字面量创建有什么区别?

在调用 new 的过程中会发生四件事情

  • 新生成了一个对象
  • 链接到原型
  • 绑定 this
  • 返回新对象

根据以上几个过程,我们也可以试着来自己实现一个 new

  • 创建一个空对象
  • 获取构造函数
  • 设置空对象的原型
  • 绑定 this 并执行构造函数
  • 确保返回值为对象
function create() {
  let obj = {}
  let Con = [].shift.call(arguments)
  obj.__proto__ = Con.prototype
  let result = Con.apply(obj, arguments)
  return result instanceof Object ? result : obj
}
  • 对于对象来说,其实都是通过 new 产生的,无论是 function Foo() 还是 let a = { b : 1 }
  • 对于创建一个对象来说,更推荐使用字面量的方式创建对象(无论性能上还是可读性)。因为你使用 new Object() 的方式创建对象需要通过作用域链一层层找到 Object,但是你使用字面量的方式就没这个问题
function Foo() {}
// function 就是个语法糖
// 内部等同于 new Function()
let a = { b: 1 }
// 这个字面量内部也是使用了 new Object()

15 instanceof 的原理

涉及面试题:instanceof 的原理是什么?

instanceof` 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 `prototype

实现一下 instanceof

  • 首先获取类型的原型
  • 然后获得对象的原型
  • 然后一直循环判断对象的原型是否等于类型的原型,直到对象原型为 null,因为原型链最终为 null
function myInstanceof(left, right) {
  let prototype = right.prototype
  left = left.__proto__
  while (true) {
    if (left === null || left === undefined)
      return false
    if (prototype === left)
      return true
    left = left.__proto__
  }
}

16 为什么 0.1 + 0.2 != 0.3

涉及面试题:为什么 0.1 + 0.2 != 0.3?如何解决这个问题?

原因,因为 JS 采用 IEEE 754双精度版本(64位),并且只要采用 IEEE 754的语言都有该问题

我们都知道计算机是通过二进制来存储东西的,那么 0.1 在二进制中会表示为

// (0011) 表示循环
0.1 = 2^-4 * 1.10011(0011)

我们可以发现,0.1 在二进制中是无限循环的一些数字,其实不只是 0.1,其实很多十进制小数用二进制表示都是无限循环的。这样其实没什么问题,但是 JS采用的浮点数标准却会裁剪掉我们的数字。

IEEE 754 双精度版本(64位)将 64 位分为了三段

  • 第一位用来表示符号
  • 接下去的 11 位用来表示指数
  • 其他的位数用来表示有效位,也就是用二进制表示 0.1 中的 10011(0011)

那么这些循环的数字被裁剪了,就会出现精度丢失的问题,也就造成了 0.1 不再是 0.1 了,而是变成了 0.100000000000000002

0.100000000000000002 === 0.1 // true

那么同样的,0.2 在二进制也是无限循环的,被裁剪后也失去了精度变成了 0.200000000000000002

0.200000000000000002 === 0.2 // true

所以这两者相加不等于 0.3 而是 0.300000000000000004

0.1 + 0.2 === 0.30000000000000004 // true

那么可能你又会有一个疑问,既然 0.1 不是 0.1,那为什么 console.log(0.1) 却是正确的呢?

因为在输入内容的时候,二进制被转换为了十进制,十进制又被转换为了字符串,在这个转换的过程中发生了取近似值的过程,所以打印出来的其实是一个近似值,你也可以通过以下代码来验证

console.log(0.100000000000000002) // 0.1

解决

parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true

17 事件机制

涉及面试题:事件的触发过程是怎么样的?知道什么是事件代理嘛?

17.1 事件触发三阶段

事件触发有三个阶段

  • window往事件触发处传播,遇到注册的捕获事件会触发
  • 传播到事件触发处时触发注册的事件
  • 从事件触发处往 window 传播,遇到注册的冒泡事件会触发

事件触发一般来说会按照上面的顺序进行,但是也有特例,如果给一个 body 中的子节点同时注册冒泡和捕获事件,事件触发会按照注册的顺序执行

// 以下会先打印冒泡然后是捕获
node.addEventListener(
  'click',
  event => {
    console.log('冒泡')
  },
  false
)
node.addEventListener(
  'click',
  event => {
    console.log('捕获 ')
  },
  true
)

17.2 注册事件

通常我们使用 addEventListener 注册事件,该函数的第三个参数可以是布尔值,也可以是对象。对于布尔值 useCapture 参数来说,该参数默认值为 falseuseCapture 决定了注册的事件是捕获事件还是冒泡事件。对于对象参数来说,可以使用以下几个属性

  • capture:布尔值,和 useCapture 作用一样
  • once:布尔值,值为 true 表示该回调只会调用一次,调用后会移除监听
  • passive:布尔值,表示永远不会调用 preventDefault

一般来说,如果我们只希望事件只触发在目标上,这时候可以使用 stopPropagation来阻止事件的进一步传播。通常我们认为 stopPropagation 是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。stopImmediatePropagation同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件。

node.addEventListener(
  'click',
  event => {
    event.stopImmediatePropagation()
    console.log('冒泡')
  },
  false
)
// 点击 node 只会执行上面的函数,该函数不会执行
node.addEventListener(
  'click',
  event => {
    console.log('捕获 ')
  },
  true
)

17.3 事件代理

如果一个节点中的子节点是动态生成的,那么子节点需要注册事件的话应该注册在父节点上

<ul id="ul">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ul>
<script>
    let ul = document.querySelector('#ul')
    ul.addEventListener('click', (event) => {
        console.log(event.target);
    })
</script>

事件代理的方式相较于直接给目标注册事件来说,有以下优点

  • 节省内存
  • 不需要给子节点注销事件

18 跨域

涉及面试题:什么是跨域?为什么浏览器要使用同源策略?你有几种方式可以解决跨域问题?了解预检请求嘛?

  • 因为浏览器出于安全考虑,有同源策略。也就是说,如果协议、域名或者端口有一个不同就是跨域,Ajax 请求会失败。
  • 那么是出于什么安全考虑才会引入这种机制呢? 其实主要是用来防止 CSRF 攻击的。简单点说,CSRF 攻击是利用用户的登录态发起恶意请求。
  • 也就是说,没有同源策略的情况下,A 网站可以被任意其他来源的 Ajax 访问到内容。如果你当前 A 网站还存在登录态,那么对方就可以通过 Ajax 获得你的任何信息。当然跨域并不能完全阻止 CSRF

然后我们来考虑一个问题,请求跨域了,那么请求到底发出去没有? 请求必然是发出去了,但是浏览器拦截了响应。你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax就不会。因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。

接下来我们将来学习几种常见的方式来解决跨域的问题

18.1 JSONP

JSONP 的原理很简单,就是利用 <script> 标签没有跨域限制的漏洞。通过 <script>标签指向一个需要访问的地址并提供一个回调函数来接收数据当需要通讯时

<script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script>
<script>
    function jsonp(data) {
        console.log(data)
    }
</script>    

JSONP 使用简单且兼容性不错,但是只限于 get 请求。

在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就需要自己封装一个 JSONP,以下是简单实现

function jsonp(url, jsonpCallback, success) {
  let script = document.createElement('script')
  script.src = url
  script.async = true
  script.type = 'text/javascript'
  window[jsonpCallback] = function(data) {
    success && success(data)
  }
  document.body.appendChild(script)
}
jsonp('http://xxx', 'callback', function(value) {
  console.log(value)
})

18.2 CORS

  • CORS 需要浏览器和后端同时支持。IE 89 需要通过 XDomainRequest 来实现。
  • 浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。
  • 服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。 虽然设置 CORS和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求和复杂请求。

简单请求

Ajax 为例,当满足以下条件时,会触发简单请求

  1. 使用下列方法之一:
  • GET
  • HEAD
  • POST
  1. Content-Type 的值仅限于下列三者之一:
  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded

请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器; XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问

复杂请求

对于复杂请求来说,首先会发起一个预检请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。

对于预检请求来说,如果你使用过 Node 来设置 CORS 的话,可能会遇到过这么一个坑。

以下以 express框架举例

app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*')
  res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS')
  res.header(
    'Access-Control-Allow-Headers',
    'Origin, X-Requested-With, Content-Type, Accept, Authorization, Access-Control-Allow-Credentials'
  )
  next()
})
  • 该请求会验证你的 Authorization 字段,没有的话就会报错。
  • 当前端发起了复杂请求后,你会发现就算你代码是正确的,返回结果也永远是报错的。因为预检请求也会进入回调中,也会触发 next 方法,因为预检请求并不包含 Authorization 字段,所以服务端会报错。

想解决这个问题很简单,只需要在回调中过滤 option 方法即可

res.statusCode = 204
res.setHeader('Content-Length', '0')
res.end()

18.3 document.domain

  • 该方式只能用于主域名相同的情况下,比如 a.test.comb.test.com 适用于该方式。
  • 只需要给页面添加 document.domain = 'test.com' 表示主域名都相同就可以实现跨域

18.4 postMessage

这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息

// 发送消息端
window.parent.postMessage('message', 'http://test.com')
// 接收消息端
var mc = new MessageChannel()
mc.addEventListener('message', event => {
  var origin = event.origin || event.originalEvent.origin
  if (origin === 'http://test.com') {
    console.log('验证通过')
  }
})

19 存储

涉及面试题:有几种方式可以实现存储功能,分别有什么优缺点?什么是 Service Worker

cookie,localStorage,sessionStorage,indexDB

特性cookielocalStoragesessionStorageindexDB
数据生命周期一般由服务器生成,可以设置过期时间除非被清理,否则一直存在页面关闭就清理除非被清理,否则一直存在
数据存储大小4K5M5M无限
与服务端通信每次都会携带在 header 中,对于请求性能影响不参与不参与不参与

从上表可以看到,cookie 已经不建议用于存储。如果没有大量数据存储需求的话,可以使用 localStoragesessionStorage 。对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage存储

对于 cookie 来说,我们还需要注意安全性。

属性作用
value如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识
http-only不能通过 JS 访问 Cookie,减少 XSS 攻击
secure只能在协议为 HTTPS 的请求中携带
same-site规定浏览器不能在跨域请求中携带 Cookie,减少 CSRF 攻击

Service Worker

  • Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全
  • Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。以下是这个步骤的实现:
// index.js
if (navigator.serviceWorker) {
  navigator.serviceWorker
    .register('sw.js')
    .then(function(registration) {
      console.log('service worker 注册成功')
    })
    .catch(function(err) {
      console.log('servcie worker 注册失败')
    })
}
// sw.js
// 监听 `install` 事件,回调中缓存所需文件
self.addEventListener('install', e => {
  e.waitUntil(
    caches.open('my-cache').then(function(cache) {
      return cache.addAll(['./index.html', './index.js'])
    })
  )
})

// 拦截所有请求事件
// 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
self.addEventListener('fetch', e => {
  e.respondWith(
    caches.match(e.request).then(function(response) {
      if (response) {
        return response
      }
      console.log('fetch source')
    })
  )
})

打开页面,可以在开发者工具中的 Application 看到 Service Worker 已经启动了

Cache 中也可以发现我们所需的文件已被缓存

当我们重新刷新页面可以发现我们缓存的数据是从 Service Worker 中读取的

20 浏览器缓存机制

注意:该知识点属于性能优化领域,并且整一章节都是一个面试题

  • 缓存可以说是性能优化中简单高效的一种优化方式了,它可以显著减少网络传输所带来的损耗。
  • 对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据。

接下来的内容中我们将通过以下几个部分来探讨浏览器缓存机制:

  • 缓存位置
  • 缓存策略
  • 实际场景应用缓存策略

20.1 缓存位置

从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络

  1. Service Worker
  2. Memory Cache
  3. Disk Cache
  4. Push Cache
  5. 网络请求

1. Service Worker

  • service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
  • Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。

2. Memory Cache

  • Memory Cache 也就是内存中的缓存,读取内存中的数据肯定比磁盘快。但是内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
  • 当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存

那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢?

  • 先说结论,这是不可能的。首先计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。内存中其实可以存储大部分的文件,比如说 JSHTMLCSS、图片等等
  • 当然,我通过一些实践和猜测也得出了一些结论:
  • 对于大文件来说,大概率是不存储在内存中的,反之优先当前系统内存使用率高的话,文件优先存储进硬盘

3. Disk Cache

  • Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。
  • 在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 ·HTTP Herder· 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据

4. Push Cache

  • Push CacheHTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。并且缓存时间也很短暂,只在会话(Session)中存在,一旦会话结束就被释放。
  • Push Cache 在国内能够查到的资料很少,也是因为 HTTP/2 在国内不够普及,但是 HTTP/2 将会是日后的一个趋势

结论

  • 所有的资源都能被推送,但是 EdgeSafari 浏览器兼容性不怎么好
  • 可以推送 no-cacheno-store 的资源
  • 一旦连接被关闭,Push Cache 就被释放
  • 多个页面可以使用相同的 HTTP/2 连接,也就是说能使用同样的缓存
  • Push Cache 中的缓存只能被使用一次
  • 浏览器可以拒绝接受已经存在的资源推送
  • 你可以给其他域名推送资源

5. 网络请求

  • 如果所有缓存都没有命中的话,那么只能发起请求来获取资源了。
  • 那么为了性能上的考虑,大部分的接口都应该选择好缓存策略,接下来我们就来学习缓存策略这部分的内容

20.2 缓存策略

通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的

20.2.1 强缓存

强缓存可以通过设置两种 HTTP Header 实现:ExpiresCache-Control 。强缓存表示在缓存期间不需要请求,state code200

Expires

Expires: Wed, 22 Oct 2018 08:41:00 GMT

ExpiresHTTP/1 的产物,表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。并且 Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效。

Cache-control

Cache-control: max-age=30
  • Cache-Control 出现于 HTTP/1.1,优先级高于 Expires 。该属性值表示资源会在 30 秒后过期,需要再次请求。
  • Cache-Control 可以在请求头或者响应头中设置,并且可以组合使用多种指令

从图中我们可以看到,我们可以将多个指令配合起来一起使用,达到多个目的。比如说我们希望资源能被缓存下来,并且是客户端和代理服务器都能缓存,还能设置缓存失效时间等

一些常见指令的作用

20.2.2 协商缓存

  • 如果缓存过期了,就需要发起请求验证资源是否有更新。协商缓存可以通过设置两种 HTTP Header 实现:Last-ModifiedETag
  • 当浏览器发起请求验证资源时,如果资源没有做改变,那么服务端就会返回 304 状态码,并且更新浏览器缓存有效期。

Last-Modified 和 If-Modified-Since

Last-Modified 表示本地文件最后修改日期,If-Modified-Since 会将 Last-Modified 的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来,否则返回 304 状态码。

但是 Last-Modified存在一些弊端:

  • 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源
  • 因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源 因为以上这些弊端,所以在 HTTP / 1.1 出现了 ETag

ETag 和 If-None-Match

  • ETag 类似于文件指纹,If-None-Match 会将当前 ETag 发送给服务器,询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来。并且 ETag优先级比 Last-Modified 高。

以上就是缓存策略的所有内容了,看到这里,不知道你是否存在这样一个疑问。如果什么缓存策略都没设置,那么浏览器会怎么处理?

对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的 Date 减去 Last-Modified 值的 10% 作为缓存时间。

20.3 实际场景应用缓存策略

频繁变动的资源

对于频繁变动的资源,首先需要使用 Cache-Control: no-cache 使浏览器每次都请求服务器,然后配合 ETag 或者 Last-Modified 来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。

代码文件

这里特指除了 HTML 外的代码文件,因为 HTML 文件一般不缓存或者缓存时间很短。

一般来说,现在都会使用工具来打包代码,那么我们就可以对文件名进行哈希处理,只有当代码修改后才会生成新的文件名。基于此,我们就可以给代码文件设置缓存有效期一年 Cache-Control: max-age=31536000,这样只有当 HTML 文件中引入的文件名发生了改变才会去下载最新的代码文件,否则就一直使用缓存

更多缓存知识详解 http://blog.poetries.top/2019/01/02/browser-cache

21 浏览器渲染原理

注意:该章节都是一个面试题。

21.1 渲染过程

1. 浏览器接收到 HTML 文件并转换为 DOM 树

当我们打开一个网页时,浏览器都会去请求对应的 HTML 文件。虽然平时我们写代码时都会分为 JSCSSHTML 文件,也就是字符串,但是计算机硬件是不理解这些字符串的,所以在网络中传输的内容其实都是 01 这些字节数据。当浏览器接收到这些字节数据以后,它会将这些字节数据转换为字符串,也就是我们写的代码。

当数据转换为字符串以后,浏览器会先将这些字符串通过词法分析转换为标记(token),这一过程在词法分析中叫做标记化(tokenization

那么什么是标记呢?这其实属于编译原理这一块的内容了。简单来说,标记还是字符串,是构成代码的最小单位。这一过程会将代码分拆成一块块,并给这些内容打上标记,便于理解这些最小单位的代码是什么意思

朗读
赞 · 0
版权属于:

一生亦木

本文链接:

https://blog.onemue.cn/high-frequency-topics.md(转载时请注明本文出处及文章链接)

评论 (1)
  1. admin 作者
    MacOS · Google Chrome

    {!{data:image/webp;base64,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}!}

    2021-06-15 回复