五天六万字,JavaScript 面试大全

5,204 阅读38分钟

一、什么是编译型语言?什么是解释型语言?JS 是什么类型的语言?

1、编译型语言

1. 解释

程序在执行之前需要一个专门的编译过程,把程序编译成 为机器语言的文件,运行时不需要重新翻译,直接使用编译的结果就行了。

2. 优缺点

程序执行效率高,依赖编译器,跨平台性差些。

3. 举例

CC++ 都是编译型语言。

2、解释型语言

1. 解释

程序不需要编译,程序在运行时才翻译成机器语言,每执 行一次都要翻译一次。

2. 优缺点

解释型语言执行效率较低,且不能脱离解释器运行,但它的跨平台型比较容易,只需提供特定解释器即可。

3. 举例

pythonJS 都是解释型语言。

二、强制类型转换 & 隐式类型转换

1、JS 中有哪些强制类型转换和隐式类型转换?

1. 强制类型转换

  • String()
  • Number()
  • Boolean()
  • parseInt()
  • parseFloat()

2. 隐式类型转换

  • + string 转为数字
  • a + " " 转为字符串
  • !var 转为布尔值 类型转换

三、基本数据类型和引用数据类型

1、区别

1. 作为函数的参数时:

  1. 基本数据类型传入的是数据的副本,原数据的更改不会影响传入后的数据。
  2. 引用数据类型传入的是数据的引用地址,原数据的更改会影响传入后的数据。

2. 内存中的存储位置:

  1. 基本数据类型存储在栈中。
  2. 引用数据类型在栈中存储了指针,该指针指向的数据实体存储在堆中。

2、栈和堆在内存中的分配

栈和堆在内存中的分配

egvar a = {name: 'yuhua'} 变量存储情况

  1. 将这句代码放入 代码区域 Code Segment
  2. 将变量 a 放入 栈(Stack):本地变量、指针
  3. {name: 'yuhua'} 放入HeapTotal(堆):对象,闭包

3、symbol

1. symbol 作为一个对象的键名时,如何获取?

不能获取 symbol 键:

  1. for infor of 循环遍历中,不会获取 symbol 键;
  2. Object.keys()Object.getOwnPropertyNames()JSON.stringify() 方法获取不到 symbol 键;

能获取 symbol 键:

  1. Object.getOwnPropertySymbols() 方法可以获取,返回一个数组;
  2. Reflect.ownKeys() 可以获取所有的键名,包括 symbol

2. symbol 的类型转换

1)可以转换为 string
const symbolKey = Symbol(123)
String(symbolKey) // "Symbol(123)"
symbolKey.toString() // "Symbol(123)"
2)可以转化为布尔值
Boolean(symbolKey) // true
3)不能转化为数字(报错)
Number(symbolKey)
Uncaught TypeError: Cannot convert a Symbol value to a number
    at Number (<anonymous>)
    at <anonymous>:1:1
4)转化为对象
b = Object(symbolKey)
Symbol {Symbol(123)}
  description: "123"
  __proto__: Symbol
    constructor: ƒ Symbol()
    description: "123"
    toString: ƒ toString()
    valueOf: ƒ valueOf()
    Symbol(Symbol.toPrimitive): ƒ [Symbol.toPrimitive]()
    Symbol(Symbol.toStringTag): "Symbol"
    get description: ƒ description()
    __proto__: Object
    [[PrimitiveValue]]: Symbol(123)
typeof b // "object"
b.constructor() // Symbol()
b instanceof Symbol // true
b instanceof Object // true
Object.prototype.toString.call(b) // "[object Symbol]"

4、字符串转函数

1. eval()

let funcStr = "function test(value){alert(value)}";
let test = eval("(false || "+funcStr+")");
test("函数能够执行");

2. new Function()

function add(a, b) {
  return a + b;
}
//等价于
var add = new Function ('a', 'b', 'return a + b');
let funcStr = "function test(value){alert(value)}";
let funcTest = new Function('return '+funcStr);
funcTest()("函数也能够执行")

四、nullundefined 的区别

1、Null

  • null 表示一个"无"的对象,转为数值为 0
  • 作为函数的参数,表示该函数的参数不是对象;
  • 作为对象原型链的终点。
  • Number(null)0
  • 5 + null5

2、Undefined

  • 变量被声明了,但是没有赋值,就等于 undefined
  • 调用函数时,应该提供的参数没有提供,该参数等于 undefined
  • 对象没有赋值属性,该属性的值为 undefined
  • 函数没有返回值时,默认返回 undefined
  • Number(undefined)NaN
  • 5 + undefinedNaN

五、typeofinstanceof 的区别

1、主要区别

  • typeof 表示对某个变量类型的检测,基本数据类型除了 null 都能正常的显示为对应的类型,引用类型除了函数会显示为 function 外,其他的都是会显示为 object
  • instanceof 用于检测某个构造函数的原型对象在不在某个对象的原型链上。

2、typeofnull 的错误显示

这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象然而 null 表示为全零,所以将它错误的判断为 object 。

3、实现一个 instanceof

Object.getPrototypeOf()Object.getPrototypeOf() 方法返回指定对象的原型(内部[[ Prototype ]]属性的值)。

function myInstance (left, right) {
	let proto = Object.getPrototypeOf(left) // Object.getPrototypeOf() 方法返回指定对象的原型(内部[[Prototype]]属性的值)。
  	while(true) {
  		if (proto === null) return false
	    if (proto === right.prototype) return true
    	proto = Object.getPrototypeOf(proto)
  	}
}

验证

myInstance([], Object) //true
myInstance(Map, Object) //true
myInstance(new Map(), Object) //true
myInstance(Map, Function) //true
myInstance(class {}, Function) //true
myInstance(1, Number) //true
myInstance('1', String) //true

六、this

1、描述下 this

对于函数而言指向最后调用函数的那个对象,是函数运行时内部自动生成的一个内部对象,只能在函数内部使用;对于全局来说,this 指向 window

2、函数内的 this 是在什么时候确定的?

函数调用时,指向最后调用的那个对象

3、callapplybind 三者的区别

三个函数的作用都是将函数绑定到上下文中,用来改变函数中 this 的指向;三者的不同点在于语法的不同。

fun.call(thisArg[, arg1[, arg2[, ...]]])
fun.apply(thisArg, [argsArray])
var bindFn = fun.bind(thisArg[, arg1[, arg2[, ...]]])
bindFn()

applycall 的区别是 call 方法接受的是若干个参数列表,而 apply 接收的是一个包含多个参数的数组。 而 bind() 方法创建一个新的函数, 当被调用时,将其 this 关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。

const name = 'window'
const sayName = function (param) {
    console.log('my name is:' + this.name + ',my param is ' + param)
}
sayName('window param') //my name is:window,my param is window param

const callObj = {
    name: 'call'
}
sayName.call(callObj, 'call param') //my name is:call,my param is call param

const applyObj = {
    name: 'apply'
}
sayName.apply(applyObj, ['apply param']) //my name is:apply,my param is apply param

const bindObj = {
    name: 'bind'
}
const bindFn = sayName.bind(bindObj, 'bind param')
bindFn() //my name is:bind,my param is bind param

4、this 的指向有哪几种?

  1. 默认绑定:全局环境中,this 默认绑定到 window
  2. 隐式绑定:一般地,被直接对象所包含的函数调用时,也称为方法调用,this 隐式绑定到该直接对象。
  3. 隐式丢失:隐式丢失是指被隐式绑定的函数丢失绑定对象,从而默认绑定到 window。显式绑定:通过 call()apply()bind() 方法把对象绑定到 this 上,叫做显式绑定。
  4. new 绑定:如果函数或者方法调用之前带有关键字 new,它就构成构造函数调用。对于 this 绑定来说,称为 new 绑定。

this 指向示意图

5、箭头函数的 this

  1. 箭头函数没有 this,所以需要通过查找作用域链来确定 this 的值,这就意味着如果箭头函数被非箭头函数包含,this 绑定的就是最近一层非箭头函数的 this
  2. 箭头函数没有自己的 arguments 对象,但是可以访问外围函数的 arguments 对象。
  3. 不能通过 new 关键字调用,同样也没有 new.target 值和原型。

6、手动实现 callapplybind

1. call

Function.prototype.myCall = function (thisArg, ...args) {
    const fn = Symbol('fn') // 声明一个独有的 symbol 属性,防止 fn 覆盖已有属性
  	thisArg = thisArg || window // 若没有 this 传入,则绑定 window 对象
  	thisArg[fn] = this // this 指向调用 call 的对象,即我们要改变 this 指向的函数
  	const result = thisArg[fn](...args) // 执行当前函数
  	delete thisArg[fn] // 删除我们声明的 fn
  	return result // 返回函数执行结果
}

2. apply

Function.prototype.myApply = function (thisArg, args) {
    const fn = Symbol('fn') // 声明一个 symbol 
  	thisArg = thisArg || window // 设置 thisArg 
  	thisArg[fn] = this // this 指向改变
  	const result = thisArg[fn](...args) // 执行函数
  	delete thisArg[fn] // 删除 fn
 	return result // 返回结果
}

3. bind

Function.prototype.myBind = function (thisArg, ...args) {
    const self = this
  	const fbound = function () {
    	self.apply(this instanceof self ? this : thisArg,args.concat(Array.prototype.slice.call(arguments)))
  	}
  	fbound.prototype = Object.create(self.prototype)
  	return fbound
}

7、判断 this 指向

1. obj0.obj.test()

const a = 1
function test () {
    console.log(this.a)
}
const obj = {
    a: 2,
    test
}
const obj0 = {
    a: 3,
    obj 
}
obj0.obj.test() // 2

2. testcopy()

var a = 1
function test () {
    console.log(this.a)
}
const obj = {
    a: 2,
    test
}
const testCopy = obj.test
testCopy() // 1 
// this 指向是在函数执行时确定

3. 在 setTimeout

var a = 1
function test () {
    console.log(this.a)
}
const obj = {
    a: 2,
    test
}
setTimeout(obj.test) // 1
// this 指向是在函数执行时确定

七、JS 模块化

1、模块化发展历程

  • IIFE 自执行函数
  • AMD 使用 requireJS 来编写模块化(依赖必须提前声明好。)
  • CMD 使用 seaJS 来编写模块化(支持动态引入依赖文件。)
  • CommonJS nodeJs 中自带的模块化
  • UMD 兼容 AMDCommonJS 语法
  • webpack(require.ensure)webpack 2.x 版本中的代码分割
  • ES ModulesES6 引入的模块化,支持 import 来引入另一个 js
  • script 标签 type="module"

js 模块化

2、AMDCMD 的区别

AMDCMD 最大的区别是对依赖模块的执行时机处理不同,注意不是加载的时机或者方式不同,二者皆为异步加载模块

  • AMD 推崇依赖前置,在定义模块的时候就要声明其依赖的模块
  • CMD 推崇就近依赖,只有在用到某个模块的时候再去 require

3、CommonJS 规范的特点

  1. 所以代码都是运行在模块作用域中,不会污染全局作用域
  2. 模块是同步加载的,只有引入的模块加载完成,才会执行后面的操作
  3. 模块在首次执行后就会缓存,再次加载只返回缓存的结果
  4. CommonJS 输出的是值的拷贝,模块内部再次改变也不会影响这个值(引用类型和基本类型有区别)

4、ES6 modules 规范有什么特点

  1. 输出使用 export
  2. 引入使用 import
  3. 可以使用 export ... from ... 来达到一个中转的效果
  4. 输入的模块变量是不可重新赋值的。只是个可读引用,但是可以改写属性
  5. exportimport 命令处于模块顶层,不能位于作用域内,处于代码块中,没法做静态优化,违背了 ES6 模块的设计初衷
  6. import 有提升效果,会提升到整个模块的头部,首先执行
  7. Babel 会把 export/import 转化为 exports/require 的形式,所以可以使用 exportsimport

5、CommonJSES6 Modules 规范的区别

  1. CommonJS 模块是运行时加载,ES6Modules 是编译时加载
  2. CommonJS 输出值的拷贝,ES6Modules 输出值的引用(模块内部改变会影响引用)
  3. CommonJS 导入模块可以是一个表达式(是使用 require() 引入),ES6Modules 导入只能是字符串
  4. CommonJS 中 this 指向当前模块,ES6Modulesthis 指向 undefined
  5. ES6Modules 中没有 argumentsrequiremoduleexports__filename__dirname 这些顶层变量

6、如何异步进行模块的加载

AMDCMD 支持异步加载模块

7、开发一个模块需要考虑哪些问题?

  1. 安全性
  2. 封闭性
  3. 避免变量冲突
  4. 隔离作用域
  5. 公共代码的抽离

8、node require(X) 引入的处理顺序是什么样的?

  1. 如果 X 是内置模块,返回该模块,不再继续执行;
  2. 如果 X'./'、'/'、'../' 开头,将根据 X 所在的父模块,确定 X 的绝对路径: a. 将 X 当成文件,依次查找,存在,返回该文件,不再继续执行; b. 将 X 当成目录,依次查找目录下的文件,存在,返回该文件,不再继续执行;
  3. 如果 X 不带有路径: a. 根据 X 所在的父模块,确定 X 可能的安装目录 b. 依次在每个目录中,将 X 当成文件名或者目录名加载
  4. 抛出 not found 错误

9、node 中相互引用

有个 a.jsb.js 两个文件,互相引用

1. CommonJS

{
  id: '...',
  exports: { ... },
  loaded: true, parent: null, filename: '', children: [], paths: []
}

CommonJS 的一个模块,就是一个脚本文件。require 命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。以后需要用到这个模块的时候,就会到 exports 属性上面取值。即使再次执行 require 命令,也不会再次执行该模块,而是到缓存之中取值。

CommonJS 重要特性是加载时执行,脚本代码在 require 时,全部执行。

CommonJS 的做法是,一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。

// a.js
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
//b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');
  1. a.js 脚本先输出一个 done 变量,然后加载另一个脚本文件 b.js。注意,此时 a.js 代码就停在这里,等待 b.js 执行完毕,再往下执行。
  2. b.js 执行到第二行,就会去加载 a.js,这时,就发生了"循环加载"。系统会去 a.js 模块对应对象的 exports 属性取值,可是因为 a.js 还没有执行完,从 exports 属性只能取回已经执行的部分,而不是最后的值。
  3. a.js 已经执行的部分,只有一行。
exports.done = false;
  1. 因此,对于 b.js 来说,它从 a.js 只输入一个变量 done,值为 false
  2. b.js 接着往下执行,等到全部执行完毕,再把执行权交还给 a.js。于是,a.js 接着往下执行,直到执行完毕。我们写一个脚本 main.js,并运行,验证这个过程。
// main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
// 运行
// 在 b.js 之中,a.done = false
// b.js 执行完毕
// 在 a.js 之中,b.done = true
// a.js 执行完毕
// 在 main.js 之中, a.done=true, b.done=true
  1. 上面的代码证明了两件事。一是,在 b.js 之中,a.js 没有执行完毕,只执行了第一行。二是,main.js 执行到第二行时,不会再次执行 b.js,而是输出缓存的 b.js 的执行结果,即它的第四行。

2. ES6

ES6 模块的运行机制与 CommonJS 不一样,它遇到模块加载命令 import 时,不会去执行模块,而是只生成一个引用。等到真的需要用到时,再到模块里面去取值。

ES6 模块是动态引用,不存在缓存值的问题,而且模块里面的变量,绑定其所在的模块。ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,以及变量总是绑定其所在的模块。

ES6 根本不会关心是否发生了"循环加载",只是生成一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

// even.js
import { odd } from './odd'
export var counter = 0;
export function even(n) {
  counter++;
  return n == 0 || odd(n - 1);
}
// odd.js
import { even } from './even';
export function odd(n) {
  return n != 0 && even(n - 1);
}

按照 CommonJS 规范,是没法加载的,是会报错的,但是 ES6 就可以执行。 之所以能够执行,原因就在于 ES6 加载的变量,都是动态引用其所在的模块。只要引用是存在的,代码就能执行。

$ babel-node
> import * as m from './even.js';
> m.even(10);
true
> m.counter
6
> m.even(20)
true
> m.counter
17

上面代码中,参数 n10 变为 0 的过程中,foo() 一共会执行 6 次,所以变量 counter 等于 6。第二次调用 even() 时,参数 n20 变为 0foo() 一共会执行 11 次,加上前面的 6 次,所以变量 counter 等于17

八、JS 事件

1、什么是事件委托

事件委托/事件代理:一般来说就是通过事件冒泡把一个元素的响应事件的函数代理到它的父层或者更外层元素上。

缺点

  1. 只能支持冒泡的事件,对于不冒泡的事件无法代理( focus/blur )
  2. 所有事件都代理容易出错,建议就近委托
  3. 内部元素层级过多,容易被某层阻止掉

2、documentwindowhtmlbody 的层级关系

window > document > html > body

  • windowBOM 的核心对象,一方面用来获取或者设置浏览器的属性和行为,一方面作为一个全局对象;
  • document 是一个跟文档相关的对象,拥有一些操作文档内容的功能;
  • html 元素 和 document 元素对象是属于 html 文档的 DOM 对象。

3、addEventListener 函数的第三个参数是什么?

1. 当为 boolean 时:

  • 第三个参数涉及到是冒泡还是捕获;
    • true 时是捕获,为 false 时是冒泡。

2. 当为 Object 时:

  • captureBoolean,表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。
  • onceBoolean,表示 listener 在添加之后最多只调用一次。如果是 truelistener 会在其被调用之后自动移除。
  • passiveBoolean,设置为 true 时,表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。
  • mozSystemGroup:只能在 XBL 或者是 Firefox' chrome 使用,这是个 Boolean,表示 listener 被添加到 system group

4、冒泡和捕获的具体过程

冒泡:当给某个元素绑定了事件之后,这个事件会依次在它的父级元素中被触发; 捕获:从上层向下层传递,与冒泡相反。

<!-- 会依次执行 button li ul -->
<ul onclick="alert('ul')">
  <li onclick="alert('li')">
    <button onclick="alert('button')">点击</button>
  </li>
</ul>
<script>
  window.addEventListener('click', function (e) {
    alert('window')
  })
  document.addEventListener('click', function (e) {
    alert('document')
  })
</script>

冒泡:button -> li -> ul -> document -> window 捕获:window -> document -> ul -> li -> button

5、有哪些不冒泡的事件

  • onblur
  • onfoucs
  • onmouseenter
  • onmouseleave

6、原生自定义事件

1. 有哪些自定义事件

  • 使用 Event
  • 使用 customEvent
  • 使用 document.createEvent('customEventName')initEvent()

2. 创建自定义事件

1)使用 Event
let myEvent = new Event('my_event_name')
2)使用 customEvent
let myEvent = new CustomEvent('my_event_name', {
    detail: {
    // 需要传递的参数
    // 在监听的回调函数中获取到:event.detail
  }
})
3)使用 document.createEvent('CustomEvent')initEvent()
let myEvent = document.createEvent('CustomEvent')
myEvent.initEvent(
    // event_name 是事件名
  // canBubble 是否冒泡
  // cancelable 是否可以取消默认行为
)

3. 事件的监听

dom.addEventListener('my_custom_name', function(e) {})

4. 事件的触发

dispatchEvent(myEvent)

5. 案例

// 1.
let myEvent = new Event('myEvent');
// 2.
let myEvent = new CustomEvent('myEvent', {
  detail: {
    name: 'lindaidai'
  }
})
// 3.
let myEvent = document.createEvent('CustomEvent');
myEvent.initEvent('myEvent', true, true)
let btn = document.getElementsByTagName('button')[0]
btn.addEventListener('myEvent', function (e) {
  console.log(e)
  console.log(e.detail)
})
setTimeout(() => {
  btn.dispatchEvent(myEvent)
}, 2000)

九、JS 内部函数和闭包

1、什么是闭包

MDN:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。

简单来说:能够读取其他函数内部变量的函数就是闭包。

for (var i = 0; i < 10; i++) {
  (function (i) {
    setTimeout(() => {
      console.log(i)
    }, 1000)
  })(i)
}

2、什么是内部函数

一般来说在一个函数内部定义另外一个函数,这样的函数就是内部函数。

3、闭包的作用?

  1. 使用闭包可以访问函数中的变量;
  2. 可以使变量长期保存在内存中。

4、内存泄露

1. 造成内存泄露的情况:

  1. 循环引用
  2. 自动类型装箱转换
  3. 某些 DOM 操作 (44. 闭包

2. 内存泄露解决方案:

  1. 低于类型转换,可以通过显示类型转换的方式来避免;
  2. 避免事件导致的循环引用;
  3. 垃圾箱操作;
  4. 对于变量的手动删除;

3. 内存泄露是内存占用很大吗?

不是,即使是 1byte 的内存,也叫内存泄露。

4. 程序中提示内存不足,是内存泄露吗?

不是,着一般是无限递归函数调用,导致栈内存溢出。

5. 内存泄露是哪个区域?

堆区。栈区不会泄露

6. 内存泄露的后果?

大多数情况下,后果不是很严重。但是过多的 DOM 操作会使网页执行速度变慢。

7. 跳转网页,内存泄露仍然存在吗?

仍然存在,直到浏览器关闭。

十、EventLoop 的执行过程

1、简述下 EventLoop 的执行过程

  • 整个 script 作为一个宏任务进行执行;
  • 执行中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列;
  • 当前宏任务执行完成之后,检测微任务列表,有则进行微任务执行,直到微任务列表全部执行完;
  • 执行浏览器的 UI 线程渲染工作;
  • 检查是否有 web worker 任务,有则执行;
  • 执行完本轮宏任务,回到第二步,依次循环,直到宏任务和微任务队列都为空。

EventLoop执行顺序

2、requestAnimationFrame

1. 特征

  1. 在重新渲染前调用。
  2. 很可能在宏任务之后不调用。

2. 为什么要在重新渲染前去调用?

因为 rAF 是官方推荐的用来做一些流畅动画所应该使用的 API,做动画不可避免的会去更改 DOM,而如果在渲染之后再去更改 DOM,那就只能等到下一轮渲染机会的时候才能去绘制出来了,这显然是不合理的。

rAF 在浏览器决定渲染之前给你最后一个机会去改变 DOM 属性,然后很快在接下来的绘制中帮你呈现出来,所以这是做流畅动画的不二选择。

3、requestIdleCallback

requestIdleCallback 方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。

1. 渲染有序进行

requestIdleCallback1

2. 渲染长期空闲

在这里插入图片描述 50ms 可以确保用户在无感知的延迟下得到回应。

4、EventLoop 循环注意点

  • 事件循环不一定每轮都伴随着重渲染,但是如果有微任务,一定会伴随着微任务执行。
  • 决定浏览器视图是否渲染的因素很多,浏览器是非常聪明的。
  • requestAnimationFrame 在重新渲染屏幕之前执行,非常适合用来做动画。
  • requestIdleCallback 在渲染屏幕之后执行,并且是否有空执行要看浏览器的调度,如果你一定要它在某个时间内执行,请使用 timeout 参数。
  • resizescroll 事件其实自带节流,它只在 Event Loop 的渲染阶段去派发事件到 EventTarget 上。

5、for 循环和 setTimeout

for 循环中加入 setTimeout

for (var i = 0; i < 10; i++) {
     setTimeout(() => {
       console.log(i)
     }, 1000)
 }

setTimeout

1. var 改成 let

for (let i = 0; i < 10; i++) {
  setTimeout(() => {
    console.log(i)
  }, 1000)
}

let

2. 使用自执行函数

for (var i = 0; i < 10; i++) {
  (function (i) {
    setTimeout(() => {
      console.log(i)
    }, 1000)
  })(i)
}

自执行函数

3. for 循环改成 forEach 循环

[1,2,3,4].forEach(item => {
  setTimeout(() => {
    console.log(item)
  }, 1000)
})

forEach

4. setTimeout 传参

for (var i = 0; i < arr.length; i++) {
  setTimeout((i) => {
    console.log(arr[i])
  }, 1000, i)
}

setTimeout 传参

5. 直接输出

for (var i = 0; i< 10; i++){
  setTimeout(console.log(i),1000);
}

直接输出

十一、JS 中的 letconstvar

1、JS 中有几种定义变量的方法?

  • let
  • const
  • var
  • class
  • import
  • function

2、letconstvar 有什么区别?

varletconst
没有块级作用域有块级作用域有块级作用域
声明全局变量在 window(全局属性下) 全局变量不在全局属性下全局变量不在全局属性下
重定义变量不会报错会报错会报错
声明变量声明变量声明一个常量
存在变量提升不存在变量提升不存在变量提升
声明之后随时赋值声明之后随时赋值声明之后立即赋值

3、const 定义常量可不可以修改?

  1. const 定义基础类型是不可以修改的;
  2. const 定义引用类型是可以修改引用类型里面的值。

4、如果我想 const 定义引用类型也不能改变它的值该怎么做?

  1. Object.freeze
  2. 代理( proxy/Object.defineProperty );
  3. 修改对象的 configurablewritable 属性。

5、如何在 ES5 的情况下实现 letconst

1. 实现 let

可以通过自执行函数。

2. 实现 const

可以通过 Object.defineProperty() 实现,设置 writable

十二、JS 数组

1、ES6 新增数组方法

Array.from()Array.of()copyWithin()find()findIndex()fill()entries()keys()values()includes()

2、ES5 新增数组方法

forEach()map()filter()some()every()indexOf()lastIndexOf()reduce()reduceRight()

3、数组的这些方法,哪些能改变原数组?

copyWithin()fill()pop()push()reverse()shift()sort()splice()

4、someevery 有什么区别?

从中文含义能看出来,some 是某些,every 是每一个,它们都返回一个 Boolean 值。

5、数组里面有 10 万条数据,取第一个元素和第 10 万个元素哪个用时长?

用时基本上一样,因为 js 里面没有数组类型,数组其实也是一个对象,keyvalue

6、数组去重你有几种方法?

1. 多层循环遍历法

  • 双重 for 循环;
  • 递归循环。

2. 利用语法自身键不可重复性或者API去重

  • ES6 Set 去重;
  • 新建空对象去重;
  • 单层循环 + filter/includes/indexOf
  • 单层循环 + MapObject 去重。

7、for 循环和 forEach 的性能哪个更好一点?

for 循环的性能更好

  • for 循环没有任何额外的函数调用栈和上下文;
  • forEach 不是普通的 for 循环的语法糖,还有诸多参数和上下文需要在执行的时候考虑进来,这里可能拖慢性能。

8、sort 排序是按照什么方式来排序的?

默认排序顺序是在将元素转换为字符串,然后比较它们的 UTF-16 代码单元值序列时构建的。

9、多维数组转为一维数组

  • reduce 递归实现
  • joinsplit 实现
  • 递归遍历
  • flat 方法
  • toStringsplit 实现
  • 广度优先遍历/深度优先遍历

10、广度优先遍历和深度优先遍历如何实现

JS深度优先遍历和广度优先遍历

1. 深度优先遍历

  • 访问顶点 v
  • 依次从 v 的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和 v 有路径相通的顶点都被访问;
  • 若此时途中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到所有顶点均被访问过为止。
const depth = (node) => {
    let stack = []
    let nodes = []
    if (node) {
        stack.push(node)
        while (stack.length) {
            //每次取最后一个
            let item = stack.pop()
            let children = item.children || []
            nodes.push(item)
            //判断children的长度
            for (let i = children.length - 1; i >= 0; i--) {
                stack.push(children[i])
            }
        }
    }
    return nodes
}

2. 广度优先遍历

  • 创建一个队列,并将开始节点放入队列中;
  • 若队列非空,则从队列中取出第一个节点,并检测它是否为目标节点;
  • 若是目标节点,则结束搜寻,并返回结果;
  • 若不是,则将它所有没有被检测过的字节点都加入队列中;
  • 若队列为空,表示图中并没有目标节点,则结束遍历。
const breadth = (node) => {
    let nodes = []
    let stack = []
    if (node) {
        stack.push(node)
        while (stack.length) {
            //取第一个
            let item = stack.shift()
            let children = item.children || []
            nodes.push(item)
            for (let i = 0; i < children.length; i++) {
                stack.push(children[i])
            }
        }
    }
    return nodes
}

11、实现一个 reduce

Array.prototype.myReduce = function (fn, init) {
    if (!init && this.length === 0) { // 如果数组长度为0
        return this
    }
    let start = 1, pre = this[0]; // 从数组第二个开始下标为1
    if (init !== undefined) { // 如果 init 字段存在,从第一个开始,下标为 0
        start = 0;
        pre = init;
    }
    for (let i = start; i < this.length; i++) { // 循环
        let current = this[i]
        pre = fn.call(this, pre, current, i, this) // 把每次的 reduce 的值返回
    }
    return pre
}

12、实现一个数组随机打乱的算法

function disOrder2 (arr) {
    for (let i = 0; i < arr.length; i++) { // 遍历
        const randomIndex = Math.floor(Math.random() * ary.length) // 生成随机数
        swap(arr, i, randomIndex)
    }
}
function swap(arr, i, _i) { // 交换
    const tem = arr[i]
    arr[i] = arr[_i]
    arr[_i] = tem  
}
arr = [1,2,3,4,5,6,7,8]
disOrder(arr)
console.log(arr)

13、给一串数字增加逗号分隔

1. 正则

num.replace(/(\d)(?=(\d{3})+(\.|$))/g, "$1,")

2. 遍历

function formatNumber(num) {
  if (!num) return "";
  let [int, float] = num.split(".");
  let intArr = int.split("");
  let result = [];
  let i = 0;
  while (intArr.length) {
    if (i !== 0 && i % 3 === 0) {
      result.unshift(intArr.pop() + ",");
    } else {
      result.unshift(intArr.pop());
    }
    i++;
  }
  return result.join("") + "." + (float ? float : "");
}

14、mapfindeverysomeforEach 等方法的第二个参数是干什么的?

arr.every(callback(element[, index[, array]])[, thisArg])
  • thisArg 执行 callback 时使用的 this 值。

十三、for infor of 有什么区别?

比较for infor of
不同点可以遍历普通对象
遍历出数组的原型对象
可以遍历出数组自身属性
遍历出来的值是 key
不可以遍历 map/set
不可以迭代 generators
IE 支持
不能遍历普通对象
不会遍历出原型对象
不会遍历自身属性
遍历出来的值是 value
可以遍历 map/set
可以迭代generators
IE 不支持
相同点可以遍历数组
可以 break 中断遍历
可以遍历数组
可以 break 中断遍历

十四、Promise

1、如何实现一个 sleep 函数(延迟函数)

通过 promisesetTimeout 来简单实现

/**
 * 延迟函数
 * @param {Number} time 时间
 */
function sleep (time = 1500) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(true)
        }, time)
    })
}

2、promise 构造函数、then 方法、catch 方法、finally 方法哪个异步哪个同步?

promise 构造函数是同步执行的,thencatchfinally 方法是异步执行的。

3、如何取消一个 promise

取消一个promise

1. 使用 promise.race()

  • Promise.race(iterable)iterable 参数里的任意一个子 promise 被成功或失败后,父 promise 马上也会用子 promise 的成功返回值或失败详情作为参数调用父 promise 绑定的相应句柄,并返回该 promise 对象。
/**
* @author guoqiankunmiss
*/
//封装一个取消promise的函数,使用promise.race的特性
function stopPromise (stopP) {
	let proObj = {};
	let promise = new Promise((resolve, reject) => {
		proObj.resolve = resolve;
		proObj.reject = reject;
	})
	proObj.promise = Promise.race([stopP, promise])
	return proObj
}
//一个5秒钟之后执行的.then方法的promise
let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(123);
    }, 5000);
});
//调用函数
let obj = stopPromise(promise);
//收集返回值
obj.promise.then(res => {
    console.log(res);
});
//两秒钟之后进行取消promise操作
setTimeout(() => {
	obj.resolve("Promise 请求被取消了!");
}, 2000)

4、多个 promise 如何获取第一个成功promise

多个Promise中获取第一个成功的Promise

1. Promise.all 改进

利用 promise.all 的特性,遍历 promise 数组,根据返回值进行判断,当成功的时候,转为 reject 返回,当失败的时候转为 resolve 继续执行。

//第一个成功的Promise
function firstProSuccess (allProMise) {
  //遍历promise数组,根据返回值进行判断,当成功的时候,转为reject返回,当失败的时候转为resolve继续执行。
  return Promise.all(allProMise.map(item => {
    return item.then(
      res => Promise.reject(res),
      err => Promise.resolve(err)
    )
  })).then(
    errors => Promise.reject(errors),
    val => Promise.resolve(val)
  )
}

2. Promise.any

  • Promise.any(iterable) 接收一个 Promise 对象的集合,当其中的一个 promise 成功,就返回那个成功的 promise 的值。

缺点:有兼容问题

5、多个 promise,所有的 promise 都取得返回结果(不管成功/失败都要返回值)

1. Promise.all 改进

和上面原理类似,只不过是当成功的时候不进行操作,当 reject 时进行 resolve 操作

2. Promise.allSettled()

  • Promise.allSettled(iterable) 返回一个在所有给定的 promise 都已经 fulfilledrejected 后的 promise

缺点:有兼容问题

6、说说 promise 的静态方法有哪些?

1. Promise.all(iterable)

接收一个 promise 数组对象(可迭代的 promise 实例对象),全部成功时,返回所有 promise 的数组集合;当其中一个失败时,返回当前失败的 promise 对象。

2. Promise.allSettled(iterable)

接收一个 promise 数组对象,全部完成时(不管成功/失败)返回新的 promise 数组集合

3. Promise.any(iterable)

接收一个 promise 数组对象,当其中任何一个成功时,返回成功的 promise

4. Promise.race(iterable)

接收一个 promise 数组对象,当其中任意一个成功/失败时,返回该 promise

5. Promise.reject(reason)

返回一个状态为失败的 Promise 对象。

6. Promise.resolve(value)

返回一个状态由给定 value 决定的 Promise 对象。

7. Promise.finally(onFinally)

在当前 promise 运行完毕后被调用,无论当前 promise 的状态是完成( fulfilled )还是失败( rejected )

8. Promise.try(f)

接收一个函数,返回一个 promise

为所有操作提供了统一的处理机制,所以如果想用 then 方法管理流程,最好都用 Promise.try 包装一下。

  • 更好的错误处理
  • 更好的互操作性
  • 易于浏览 Promise-try

7、Promise.then 的第二个参数有了解吗?和 .catch 有什么区别?

then() 方法返回一个 Promise

它最多需要有两个参数Promise 的成功和失败情况的回调函数。

p.then(onFulfilled[, onRejected]);
p.then(value => {
  // fulfillment
}, reason => {
  // rejection
});

第二个参数也是一个函数,是对失败情况的回调函数。

then 第二个参数catch
then 方法的参数Promise 的实例方法
then 的第一个参数抛出异常捕获不到then 的第一个参数抛出异常可以捕获
是一个函数本质是 then 方法的语法糖
如果第二个参数和 catch 同时存在,promise 内部报错,第二个参数可以捕获此时,catch 捕获不到,第二个参数不存在,catch 才会捕获到
不建议使用建议使用 catch 进行错误捕获

8、Promise.resolve 有几种情况?

1. 参数是一个 Promise 实例

参数是 Promise 实例,那么 Promise.resolve 将不做任何修改、原封不动地返回这个实例。

2. 参数是一个 thenable 对象

Promise.resolve() 方法会将这个对象转为 Promise 对象,然后就立即执行 thenable 对象的 then() 方法。

3. 参数不是具有 then() 方法的对象,或根本就不是对象

如果参数是一个原始值,或者是一个不具有 then() 方法的对象,则 Promise.resolve() 方法返回一个新的 Promise 对象,状态为 resolved

4. 不带有任何参数

直接返回一个 resolved 状态的 Promise 对象。

9、如果 .then 中的参数不是函数,那会怎样?

Promise.resolve(1)
    .then(2)
    .then(console.log)
// 1

如果 .then 中的参数不是函数,则会在内部被替换为 (x) => x,即原样返回 promise 最终结果的函数。

10、如果 .finally 后面继续跟了个 .then,那么这个 then 里面的值是什么?

Promise.resolve('resolve')
  .finally(() => {
    console.log('this is finally')
    return 'finally value'
  })
  .then(res => {
    console.log('finally后面的then函数, res的值为:', res)
  })
// this is finally

finally 后面的 then 函数, res 的值为: resolve

  1. finally 的回调函数中不接收任何参数;
  2. promise 结束时,无论结果是 fulfilled 或者是 rejected,都会执行 finally 回调函数;
  3. finally 返回的是一个上一次的 Promise 对象值。

11、.all.race 在传入的数组有第一个抛出异常的时候,其他异步任务还会继续执行吗?

会的,会继续执行,只是不会在 then / catch 中表现出来。

浏览器执行下面代码,可以看出当报错的时候 console 还是会继续执行的,只是在 对应的回调函数里面没有表现出来。

function sleep (n) {
    return new Promise((resolve, reject) => {
        console.log(n)
        Math.random() > 0.5 ? reject(n) : resolve(n)
    }, n % 2 === 0 ? 1000 * n : 1000)
}
Promise.all([sleep(1), sleep(2), sleep(3)])
  .then(res => console.log('all res: ', res))
  .catch(err => console.log('all err:', err))
Promise.race([sleep(1), sleep(2), sleep(3)])
  .then(res => console.log('race res: ', res))
  .catch(err => console.log('race err:', err))

12、.all 是并发的还是串行的?

是并发的,但是返回值和 promise.all 中接收到的数组顺序一样。

13、promise 为什么可以进行链式调用

因为 thencatchfinally 方法会返回一个新的 promise,所以允许我们进行链式调用。

14、async/await

1. 实现原理

async 函数是基于 generator 实现,所以涉及到 generator 相关知识。 在没有async 函数之前,通常使用 co 库来执行 generator,所以通过 co 我们也能模拟 async 的实现。

2. 简单实现

1)co
function Asyncfn() {
  return co(function*() {
    //.....
  });
}
function co(gen) {
  return new Promise((resolve, reject) => {
    const fn = gen();
    function next(data) {
      let { value, done } = fn.next(data);
      if (done) return resolve(value);
      Promise.resolve(value).then(res => {
        next(res);
      }, reject);
    }
    next();
  });
}
2)Generator 函数和自执行器
function spawn(genF) {
    return new Promise(function(resolve, reject) {
        const gen = genF();
        function step(nextF) {
            let next;
            try {
                next = nextF();
            } catch (e) {
                return reject(e);
            }
            if (next.done) {
                return resolve(next.value);
            }
            Promise.resolve(next.value).then(
                function(v) {
                    step(function() {
                        return gen.next(v);
                    });
                },
                function(e) {
                    step(function() {
                        return gen.throw(e);
                    });
                }
            );
        }
        step(function() {
            return gen.next(undefined);
        });
    });
}

十五、说下 JSON.stringifyJSON.parse

1、JSON.stringify

定义:将一个 JavaScript 对象或值转换为 JSON 字符串。 参数:有三个参数

JSON.stringify(value[, replacer [, space]])
  1. replacer replacer 参数可以是一个函数或者一个数组。 作为函数,它有两个参数,键( key )和值( value ),它们都会被序列化。 replacer 是一个数组,数组的值代表将被序列化成 JSON 字符串的属性名。
  2. space space 参数用来控制结果字符串里面的间距。 如果是一个数字, 则在字符串化时每一级别会比上一级别缩进多这个数字值的空格; 如果是一个字符串,则每一级别会比上一级别多缩进该字符串。

2、JSON.parse

定义:用来解析 JSON 字符串。 参数:有两个参数

JSON.parse(text[, reviver])
  1. reviver 转换器, 如果传入该参数(函数),可以用来修改解析生成的原始值。

特性

  1. 转换值如果有 toJSON() 方法,该方法定义什么值将被序列化。
  2. 非数组对象的属性不能保证以特定的顺序出现在序列化后的字符串中。
  3. 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
  4. undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。函数、undefined 被单独转换时,会返回 undefined,如JSON.stringify(function(){}) or JSON.stringify(undefined)
  5. 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
  6. 所有以 symbol 为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们。
  7. Date 日期调用了 toJSON() 将其转换为了 string 字符串(同Date.toISOString()),因此会被当做字符串处理。
  8. NaNInfinity 格式的数值及 null 都会被当做 null
  9. 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性。

十六、=====Object.is()

1、区别

  • == 两边值类型不同的时候,先进行类型转换,在比较
  • === 不进行类型转换,直接值比较
  • Object.is(val1, val2) 判断两个值是否为同一值

2、== 类型转换是怎么转换的?

  1. 如果类型不同,进行类型转换
  2. 判断比较的是否是 null 或者是 undefined,如果是,返回 true
  3. 判断类型是否为 string 或者 number,如果是,将 string 转换为 number
  4. 判断其中一方是否为 boolean,如果是,将其中一方转为 number 在进行判断
  5. 判断其中一方是否为 object,且另外一方是 stringnumbersymbol,如果是,将 object 转为原始类型进行判断(valueOf() 方法)
  6. 如果有一个是 NaN,则直接返回 false
  7. 如果两个都是对象,则比较是否指向同一个对象 ==比较

3、[] == ![] 的值为什么?

答案:为 true

转换步骤

  1. ! 运算符优先级最高,![] 会被转换为 false,因此此时为 [] == false
  2. 根据第四条,其中一方为 boolean,把 boolean 转为 number,所以此时为 [] == 0
  3. 再根据第五条,把数组 [] 转为原始类型,调用数组的 toString() 方法,[].toString() = '',所以此时为 '' == 0
  4. 再根据第三条,把 string 转为 number'' 转为 number 为 0,所以此时 0 == 0
  5. 两边数据类型相同 0 == 0,为 true

4、Object.is() 判断两值相等的情况

不会进行强制类型转换

  • 都是 undefined
  • 都是 null
  • 都是 truefalse
  • 都是相同长度的字符串且相同字符按相同顺序排列
  • 都是相同对象(意味着每个对象有同一个引用)
  • 都是数字且
    • 都是 +0
    • 都是 -0
    • 都是 NaN
    • 或都是非零而且非 NaN 且为同一个值

十七、防抖和节流

1、什么是防抖和节流

防抖:是多次执行改为最后一次执行 节流:是将多次执行改为每隔一段时间执行

2、简单实现防抖和节流

1. 防抖实现

思路: 触发高频事件后 n 秒内函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间,每次触发事件时都取消之前的延时调用方法

function debounce (fn, time = 500) {
  let timeout = null; // 创建一个标记用来存放定时器的返回值
  return function () {
    clearTimeout(timeout) // 每当触发时,把前一个 定时器 clear 掉
    timeout = setTimeout(() => { // 创建一个新的 定时器,并赋值给 timeout
      fn.apply(this, arguments)
    }, time)
  }
}
function testDebounce () {
  console.log('测试防抖')
}
const inp = document.getElementById('testInp')
inp.addEventListener('input', debounce(testDebounce))

2. 节流实现

高频事件触发,但在 n 秒内只会执行一次,所以节流会稀释函数的执行频率,每次触发事件时都判断当前是否有等待执行的延时函数

function throttle (fn, time = 100) {
  let timeout;
  return function () {
    let context = this
    let args = arguments
    if (!timeout) {
      timeout = setTimeout(() => {
        timeout = null
        fn.apply(context, args)
      }, time)
    }
  }
}
function testThro () {
  console.log('测试节流')
}
const inp = document.getElementById('testInp')
inp.addEventListener('input', throttle(testThro))

十八、cookiesessionStoragelocalStorage

1、三者区别

  • cookie 用来保存登录信息,大小限制为 4KB 左右
  • localStorageHtml5 新增的,用于本地数据存储,保存的数据没有过期时间,一般浏览器大小限制在 5MB
  • sessionStorage 接口方法和 localStorage 类似,但保存的数据的只会在当前会话中保存下来,页面关闭后会被清空。
名称生命期大小限制与服务器通信是否可以跨域
cookie一般由服务器生成,可设置失效时间。如果在浏览器端生成 Cookie,默认是关闭浏览器后失效4KB每次都会携带在 HTTP 头中,如果使用 cookie 保存过多数据会带来性能问题一般不可,相同 domain 下可以允许接口请求携带 cookie
localStorage除非被清除,否则永久保存5MB仅在浏览器中保存,不与服务器通信不可
sessionStorage仅在当前会话下有效,关闭页面或浏览器后被清除5MB仅在浏览器中保存,不与服务器通信不可

2、localStorage 进行怎么进行跨域存储?

localStorage 是不可以进行跨域操作的,但是想进行跨域操作可以使用 postMessagewebsocket 进行变相的跨域操作。

十九、浏览器跨域问题

1、什么是浏览器同源策略?

同源策略是一个重要的安全策略,它用于限制一个 origin 的文档或者它加载的脚本如何能与另一个源的资源进行交互,它能帮助阻隔恶意文档,减少可能被攻击的媒介。

所谓同源策略,是指只有在地址的:

  1. 协议名
  2. 域名
  3. 端口名

均一样的情况下,才允许访问相同的 cookielocalStorage,以及访问页面的 DOM 或是发送 Ajax 请求。

2、没有同源策略限制有哪些危险场景?

  • ajxa 请求
  • Dom 的查询 同源策略确实能规避一些危险,不是说有了同源策略就安全,只是说同源策略是一种浏览器最基本的安全机制,毕竟能提高一点攻击的成本。

3、为什么浏览器会禁止跨域?

  • 跨域只存在浏览器端,因为浏览器的形态很开放,需要对它进行限制。
  • 同源策略用于保护用户信息安全,防止恶意窃取数据(ajax 同源策略、Dom 同源策略)。

4、跨域有哪些解决方式?

CSDN 跨域问题解决

  1. jsonp
  2. cors
  3. postMessage
  4. websocket
  5. Node 中间件代理(两次跨域)
  6. nginx 反向代理
  7. window.name + iframe
  8. location.hash + iframe
  9. document.domain + iframe

5、CORS 常用的配置有哪些?

  • Access-Control-Allow-Origin 允许的域名
  • Access-Control-Allow-Methods 允许的 http 请求方法
  • Access-Control-Allow-Headers 支持的请求头
  • Access-Control-Allow-Credentials 是否发送 cookie
  • Access-Control-Max-Age 以秒为单位的缓存时间

6、CORS 跨域的判定流程

  1. 浏览器先判断是否同源,若同源,直接发送数据,否则,发送跨域请求;
  2. 服务器收到跨域请求后,根据自身配置返回对应的文件头;
  3. 浏览器根据收到的响应头里的 Access-Control-Allow-origin 字段进行匹配,若无该字段说明不允许跨域,报错,有该字段进行比对,判断是否可以跨域。

7、什么是简单请求?

简单请求是指满足以下条件的:

  • 使用 getposthead 其中一种方法进行请求的;
  • http 的头信息不超出一下情况:
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID Content-Type:值仅限于 application/x-www-form-urlencodedmultipart/form-datatext/plain
  • 请求中 XMLHttpRequestUpload 对象没有注册任何的事件监听器;
  • XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。 请求中没有使用 ReadableStream 对象。

8、非简单请求

对服务器有特殊要求的请求(简单请求之外就是非简单请求)。

例如:请求方式是 putdeleteContent-Type 的类型是 application/json

非简单请求会在正式通信前使用 options 发起一个预检请求,询问服务器当前的域名是否在服务器允许的名单之中,以及使用哪些头信息字段等。

9、有哪些方法可一嵌入跨源的资源?

  • script 标签,嵌入跨域脚本;
  • link 标签,嵌入 css
  • img 标签,嵌入图片;
  • video/audio 标签,嵌入视频、音频;
  • object/embed/applet 标签,嵌入 svg /图片 等;
  • svg 标签,嵌入 svg
  • 通过 @font-face 嵌入字体;
  • 通过 iframe 嵌入资源

10、手动实现一个 JSONP

//Promise封装
function jsonp({ url, params, callback }) {
  return new Promise((resolve, reject) => {
    // 创建 script 标签
    let script = document.createElement('script')
    // 把 callback 挂载在 window 上,执行之后删除 script 
    window[callback] = function(data) {
      resolve(data)
      document.body.removeChild(script)
    }
    // 添加参数
    params = { ...params, callback } // wd=b&callback=callFun
    let arrs = []
    for (let key in params) {
      arrs.push(`${key}=${params[key]}`)
    }
    // 设置 script 的 URL
    script.src = `${url}?${arrs.join('&')}`
    // 插入 body 中
    document.body.appendChild(script)
  })
}
// 调用示例
jsonp({
  url: 'http://localhost:3000/code',
  params: { wd: 'hello world' },
  callback: 'callFun'
}).then(data => {
  console.log(data) // 你好啊
  //再此回调结束后删除该script
})

二十、说下 js 的继承方式

JS 常见的六种继承方式

1、原型链继承 prototype

子类型的原型为父类型的一个实例对象。

Child.prototype = new Parent()

优点:

  1. 继承方式简单
  2. 父类新增方法、属性,子类都能访问到 缺点:
  3. 无法实现多继承
  4. 来自父类的所有属性被所有实例共享
  5. 要想为子类新增属性和方法,必须要在Child.prototype = new Parent() 之后,因为会被覆盖
  6. 创建子类时,不能像父类传递参数

2、构造函数继承 call

在子类型构造函数中通用 call() 调用父类型构造函数

function Child(name, age, price) {
    Parent.call(this, name, age)  // 相当于: this.Parent(name, age)
}

优点:

  1. 原型链继承中子类实例共享父类引用属性的问题
  2. 创建子类实例时,可以向父类传递参数
  3. 可以实现多继承(call多个父类对象) 缺点:
  4. 实例并不是父类的实例,只是子类的实例
  5. 只能继承父类的实例属性和方法,不能继承原型属性和方法
  6. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

3、原型链+构造函数的组合继承 prototype + call

调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用。

function Child (name, age, price) {
  Parent.call(this, name, age)
}
Child.prototype = new Parent()
Child.prototype.constructor = Child//组合继承也是需要修复构造函数指向的

优点:

  1. 可以继承实例属性/方法,也可以继承原型属性/方法
  2. 不存在引用属性共享问题
  3. 可传参 缺点:
  4. 调用了两次父类构造函数,生成了两份实例

4、组合继承优化1

通过父类原型和子类原型指向同一对象,子类可以继承到父类的公有方法当做自己的公有方法,而且不会初始化两次实例方法/属性,避免的组合继承的缺点。

function Child (name, age, price) {
    Parent.call(this, name, age)
}
Child.prototype = Parent.prototype

优点:

  1. 不会调用了两次父类构造函数 缺点:
  2. 没办法辨别是实例是子类还是父类创造的,子类和父类的构造函数指向是同一个。

5、组合继承优化2

借助原型可以基于已有的对象来创建对象,var B = Object.create(A)A 对象为原型,生成了 B 对象。B 继承了 A 的所有属性和方法。

function Child (name, age, price) {
    Parent.call(this, name, age)
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child

6、es6 class 的继承

class 关键字只是原型的语法糖,JavaScript 继承仍然是基于原型实现的。

class Parent {
    constructor(name, age) {
        this.name = name
        this.age = age
    }
    setName () {
        console.log('parent')
    }
}
let child1 = new Parent('name1', 18)
let child2 = new Parent('name2', 16)
class Child extends Parent {
    constructor(name, age, price) {
        super(name, age)
        this.price = price
    }
    setAge () {
        console.log('子类方法')
    }
}
let child3 = new Child('name3', 20, 15000)
let child4 = new Child('name4', 21, 10000)

优点:

  1. 简单继承

二十一、排序算法

1、冒泡排序

简单来说就是相邻两个元素进行对比,按照你需要的排序方式(升序or降序)进行位置替换,替换时需要额外一个变量当作中间变量去暂存值。 冒泡排序

function bubbleSort(arr) {
    var len = arr.length;
    for (var i = 0; i < len; i++) {
        for (var j = 0; j < len - 1 - i; j++) {
            if (arr[j] > arr[j+1]) {        //相邻元素两两对比
                var temp = arr[j+1];        //元素交换
                arr[j+1] = arr[j];
                arr[j] = temp;
            }
        }
    }
    return arr;
}

2、快速排序

选择一个基准,将比基准小的放左边,比基准小的放在右边(基准处在中间位置) 快速排序

function quickSort(arr) {
    //如果数组<=1,则直接返回
    if (arr.length <= 1) { return arr; }
    var pivotIndex = Math.floor(arr.length / 2);
    //找基准,并把基准从原数组删除
    var pivot = arr.splice(pivotIndex, 1)[0];
    //定义左右数组
    var left = [];
    var right = [];
    //比基准小的放在left,比基准大的放在right
    for (var i = 0; i < arr.length; i++) {
        if (arr[i] <= pivot) {
            left.push(arr[i]);
        }
        else {
            right.push(arr[i]);
        }
    }
    //递归
    return quickSort(left).concat([pivot], quickSort(right));
}

3、选择排序

首先从原始数组中找到最小的元素,并把该元素放在数组的最前面,然后再从剩下的元素中寻找最小的元素,放在之前最小元素的后面,直到排序完毕 选择排序

function selectionSort(arr) {
    var len = arr.length;
    var minIndex, temp;
    for (var i = 0; i < len - 1; i++) {
      minIndex = i;
      for (var j = i + 1; j < len; j++) {
        if (arr[j] < arr[minIndex]) {
          minIndex = j;
        }
      }
      temp = arr[i];
      arr[i] = arr[minIndex];
      arr[minIndex] = temp;
    }
    return arr;
}

4、插入排序

从第二个元素开始(假定第一个元素已经排序了),取出这个元素,在已经排序的元素中从后向前进行比较,如果该元素大于这个元素,就将该元素移动到下一个位置,然后继续向前进行比较,直到找到小于或者等于该元素的位置,将该元素插入到这个位置后.重复这个步骤直到排序完成 插入排序

function insertionSort(arr) {
  var len = arr.length;
  var preIndex, current;
  for (var i = 1; i < len; i++) {
    preIndex = i - 1;
    current = arr[i];
    while (preIndex >= 0 && arr[preIndex] > current) {
      arr[preIndex + 1] = arr[preIndex];
      preIndex--;
    }
    arr[preIndex + 1] = current;
  }
  return arr;
}

5、归并排序

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。 归并排序

function mergeSort(arr) {  //采用自上而下的递归方法
    var len = arr.length;
    if(len < 2) {
        return arr;
    }
    var middle = Math.floor(len / 2),
        left = arr.slice(0, middle),
        right = arr.slice(middle);
    return merge(mergeSort(left), mergeSort(right));
}
function merge(left, right){
    var result = [];
    console.time('归并排序耗时');
    while (left.length && right.length) {
        if (left[0] <= right[0]) {
            result.push(left.shift());
        } else {
            result.push(right.shift());
        }
    }
    while (left.length)
        result.push(left.shift());
    while (right.length)
        result.push(right.shift());
    console.timeEnd('归并排序耗时');
    return result;
}

6、希尔排序

利用步长来进行两两元素比较,然后缩减步长在进行排序。 说明:希尔排序的实质是分组插入排序,该方法又称缩小增量排序。该方法的基本思想是:先将整个待排元素序列分割为若干个子序列(由相隔某个‘增量’的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,带这个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况)效率是很高的,因此希尔排序在时间效率上有较大的提高。 与插入排序的不同之处:它会优先比较距离较远的元素 希尔排序

function shellSort(arr) {
  let temp,
    gap = 1;
  while (gap < arr.length / 3) {
    gap = gap * 3 + 1//动态定义间隔序列
  }
  for (gap; gap > 0; gap = Math.floor(gap / 3)) {//控制步长(间隔)并不断缩小
    for (var i = gap; i < arr.length; i++) {//按照增量个数对序列进行排序
      temp = arr[i]
      for (var j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {//例:j=0  arr[1]>arr[5]
        arr[j + gap] = arr[j]
      }
      arr[j + gap] = temp
    }
  }
  return arr
}

7、各类排序算法比较

算法比较

二十二、时间复杂度、空间复杂度

1、如何去衡量不同算法之间的优劣呢?

主要还是从算法所占用的「时间」和「空间」两个维度去考量。

  • 时间维度:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述。
  • 空间维度:是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述。

2、时间复杂度

1. 表示方法

大O符号表示法 」,即 T(n) = O(f(n))

时间复杂度的公式是: T(n) = O( f(n) ),其中 f(n) 表示每行代码执行次数之和,而 O 表示正比例关系,这个公式的全称是:算法的渐进时间复杂度。

2. 常见的复杂度量级

• 常数阶 O(1) • 对数阶 O(logN) • 线性阶 O(n) • 线性对数阶 O(nlogN) • 平方阶 O(n²) • 立方阶 O(n³) • K次方阶 O(n^k) • 指数阶 (2^n)

3. O(1)

无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是 O(1)

var i = 1;
var j = 2;
++i;
j++;
var m = i + j;

4. O(n)

for 循环里面的代码会执行 n 遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用 O(n) 来表示它的时间复杂度。

for(i=1; i<=n; ++i)
{
   j = i;
   j++;
}

5. 对数阶 O(logN)

var i = 1;
while(i<n)
{
    i = i * 2;
}

while 循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。我们试着求解一下,假设循环 x 次之后,i 就大于 2 了,此时这个循环就退出了,也就是说 2x 次方等于 n,那么 x = log2^n 也就是说当循环 log2^n 次以后,这个代码就结束了。因此这个代码的时间复杂度为:O(logn)

6. O(nlogN)

将时间复杂度为 O(logn) 的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了 O(nlogN)

for(m=1; m<n; m++)
{
    i = 1;
    while(i<n)
    {
        i = i * 2;
    }
}

7. O(n²)O(m*n)O(n³)O(n^k)

平方阶 O(n²)O(n) 的代码再嵌套循环一遍,它的时间复杂度就是O(n*n),即 O(n²)

for(x=1; i<=n; x++)
{
   for(i=1; i<=n; i++)
    {
       j = i;
       j++;
    }
}

O(m*n) 将其中一层循环的 n 改成 m,那它的时间复杂度就变成了 O(m*n)

for(x=1; i<=m; x++)
{
   for(i=1; i<=n; i++)
    {
       j = i;
       j++;
    }
}

3、空间复杂度

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势,我们用 S(n) 来定义。

1. 常见的复杂度量级

空间复杂度比较常用的有:O(1)、O(n)、O(n²)

2. O(1)

如果算法执行所需要的临时空间不随着某个变量 n 的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1)

var i = 1;
var j = 2;
++i;
j++;
var m = i + j;

代码中的 ijm 所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)

3. O(n)

var arr = [1, 2, 3]
for(i=1; i<=arr.lemgth; ++i)
{
   j = i;
   j++;
}

第一行定义了一个数组出来,这个数据占用的大小为 n,这段代码的 2-6行,虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)

二十三、接口请求

1、AJAX

1、简单实现一个 ajax

function stringify (json) {
  var str = "";
  for (var i in json) {
    str += i + "=" + json[i] + "&";
  }
  return str.slice(0, -1);
}
function myAjax (type, url, params, callback, errback) {
  let xhr = null;
  //表IE
  if (window.XMLHttpRequest) {
    xhr = new XMLHttpRequest();
  } else {
    xhr = new ActiveXObject("Microsoft.XMLHTTP");
  }
  if (type == "get") {
    xhr.open(type, url + "?" + stringify(params), true);
    xhr.send();
  } else {
    xhr.open(type, url, true);
    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    //json转换成name=张三&age=1
    xhr.send(stringify(params));
  }
  xhr.onreadystatechange = function () {
    // 表示请求已完成
    if (xhr.readyState == 4) {
      if (xhr.status == 200) {
        if (callback) {
          callback(xhr.responseText);
        }
      } else {
        errback && errback();
      }
    }
  }
}

2、ajaxreadyState 的状态

  • 0 未初始化,还没有调用 open() 方法
  • 1 启动,已经调用 open() 方法,但是没有调用 send() 方法
  • 2 发送,已经调用 send() 方法,但是尚未接收响应
  • 3 接收,已经接收到部分响应数据
  • 4 完成,已经接收到全部响应数据

2、Axios

Axios 本质上也是对原生 XHR 的封装,只不过它是 Promise 的实现版本,符合最新的 ES 规范

1. 特性:

  • node.js 创建 http 请求
  • 支持 Promise API
  • 客户端支持防止 CSRF
  • 提供了一些并发请求的接口(重要,方便了很多的操作)

3、Fetch

Fetch API 提供了一个 JavaScript 接口,用于访问和操纵 HTTP 管道的部分,例如请求和响应。它还提供了一个全局 fetch() 方法,该方法提供了一种简单,合理的方式来跨网络异步获取资源。

1. 优点

  • 语法简洁,更加语义化
  • 基于标准 Promise 实现,支持 async/await
  • 同构方便,使用 isomorphic-fetch

2. 缺点

  • Fetch 请求默认是不带 cookie 的,需要设置 fetch(url, {credentials: 'include'})
  • 服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject

二十四、new 操作符

1、new 的实现流程

  • 1.创建一个新对象;
  • 2.将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
  • 3.执行构造函数中的代码(为这个新对象添加属性);
  • 4.返回新对象。
  • 5.将构造函数的 prototype 关联到实例的 __proto__

2、如何实现一个 new

function myNew (foo, ...args) {
  // 创建一个新对象,并继承 foo 的 prototype 属性
    let obj = Object.create(foo.prototype)
  // 执行构造方法,并绑定新 this,
  let result = foo.apply(obj, args)
  // 如果构造方法返回了一个对象,那么就返回该对象,否则就返回 myNew 创建的新对象
  return Object.prototype.toString().call(result) === '[object Object]' ? result : obj
}

二十五、网页全屏怎么实现?

document.documentElement.requestFullscreen()

需要兼容实现

1、网页全屏

function fullScreen() {
    if (!document.fullscreenElement &&
        !document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement) { // current working methods
        if (document.documentElement.requestFullscreen) {
            document.documentElement.requestFullscreen();
        } else if (document.documentElement.msRequestFullscreen) {
            document.documentElement.msRequestFullscreen();
        } else if (document.documentElement.mozRequestFullScreen) {
            document.documentElement.mozRequestFullScreen();
        } else if (document.documentElement.webkitRequestFullscreen) {
            document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
        }
    }
}

2、取消网页全屏

function exitFullScreen() {
    if (document.exitFullscreen) {
        document.exitFullscreen();
    } else if (document.msExitFullscreen) {
        document.msExitFullscreen();
    } else if (document.mozCancelFullScreen) {
        document.mozCancelFullScreen();
    } else if (document.webkitExitFullscreen) {
        document.webkitExitFullscreen();
    }
}

3、检测是否全屏

/**
 * 检查是否全屏
 * @return {[Boolean]} [是否全屏,为 true 没有全屏,false 全屏]
 */
function checkFullScreenValue () {
    return !document.fullscreenElement &&
        !document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement
}

二十六、MapWeakMapsetWeakSet 有什么区别?

WeakMapWeakSet 都是弱引用

1、什么是弱引用

弱引用是指不能确保其引用的对象不会被垃圾回收器回收的引用,换句话说就是可能在任意时间被回收。

弱引用随时都会消失,遍历机制无法保证成员的存在

2、Set

  • 遍历顺序:插入顺序
  • 没有键只有值,可认为键和值两值相等
  • 添加多个 NaN 时,只会存在一个 NaN
  • 添加相同的对象时,会认为是不同的对象
  • 添加值时不会发生类型转换(5 !== "5")
  • keys()values() 的行为完全一致,entries() 返回的遍历器同时包括键和值且两值相等

3、weakSet 作用

  • Set 结构类似,成员值只能是对象
  • 储存 DOM 节点:DOM 节点被移除时自动释放此成员,不用担心这些节点从文档移除时会引发内存泄漏
  • 临时存放一组对象或存放跟对象绑定的信息:只要这些对象在外部消失,它在 WeakSet 结构中的引用就会自动消
  • 成员都是弱引用,垃圾回收机制不考虑 WeakSet 结构对此成员的引用
  • 成员不适合引用,它会随时消失,因此 ES6 规定 WeakSet 结构不可遍历
  • 其他对象不再引用成员时,垃圾回收机制会自动回收此成员所占用的内存,不考虑此成员是否还存在于 WeakSet 结构中

4、Map

  • 遍历顺序:插入顺序
  • 对同一个键多次赋值,后面的值将覆盖前面的值
  • 对同一个对象的引用,被视为一个键
  • 对同样值的两个实例,被视为两个键
  • 键跟内存地址绑定,只要内存地址不一样就视为两个键
  • 添加多个以 NaN 作为键时,只会存在一个以 NaN 作为键的值
  • Object 结构提供字符串—值的对应,Map 结构提供值—值的对应

5、WeakMap

  • Map 结构类似,成员键只能是对象
  • 储存 DOM 节点:DOM 节点被移除时自动释放此成员键,不用担心这些节点从文档移除时会引发内存泄漏
  • 部署私有属性:内部属性是实例的弱引用,删除实例时它们也随之消失,不会造成内存泄漏
  • 成员键都是弱引用,垃圾回收机制不考虑 WeakMap 结构对此成员键的引用
  • 成员键不适合引用,它会随时消失,因此 ES6 规定 WeakMap 结构不可遍历
  • 其他对象不再引用成员键时,垃圾回收机制会自动回收此成员所占用的内存,不考虑此成员是否还存在于 WeakMap 结构中
  • 一旦不再需要,成员会自动消失,不用手动删除引用
  • 弱引用的只是键而不是值,值依然是正常引用
  • 即使在外部消除了成员键的引用,内部的成员值依然存在

6、Object 转为 Map

let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj))

二十七、Proxy

1、语法

  • target 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理
  • handler 一个通常以函数作为属性的对象,用来定制拦截行为 代理只会对 proxy 对象生效,对代理对象没有任何效果
origin = {}
obj = new Proxy(origin, {
  get: function (target, propKey, receiver) {
        return '10'
  }
});
obj.a // 10
obj.b // 10
origin.a // undefined
origin.b // undefined

2、Handler 对象常用的方法

方法描述
handler.has()in 操作符的捕捉器。
handler.get()属性读取操作的捕捉器。
handler.set()属性设置操作的捕捉器。
handler.deleteProperty()delete 操作符的捕捉器。
handler.ownKeys()Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。
handler.apply()函数调用操作的捕捉器。
handler.construct()new 操作符的捕捉器

3、proxy 代理是否可以撤销?

proxy 有一个唯一的静态方法,Proxy.revocable(target, handler) Proxy.revocable() 方法可以用来创建一个可撤销的代理对象 该方法的返回值是一个对象,其结构为: {"proxy": proxy, "revoke": revoke}

  • proxy 表示新生成的代理对象本身,和用一般方式 new Proxy(target, handler) 创建的代理对象没什么不同,只是它可以被撤销掉。
  • revoke 撤销方法,调用的时候不需要加任何参数,就可以撤销掉和它一起生成的那个代理对象。
const target = { name: 'vuejs'}
const {proxy, revoke} = Proxy.revocable(target, handler)
proxy.name // 正常取值输出 vuejs
revoke() // 取值完成对proxy进行封闭,撤消代理
proxy.name // TypeError: Revoked //已撤销

二十八、执行上下文

1、执行上下文的类型?

1. 全局执行上下文:

一个程序中只能存在一个全局执行上下文。

这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它做了两件事:

  1. 创建一个全局对象,在浏览器中这个全局对象就是 window 对象。
  2. this 指针指向这个全局对象。

2. 函数执行上下文:

可以有无数个函数执行上下文。

每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。

3. Eval 函数执行上下文:

jseval 函数执行其内部的代码会创建属于自己的执行上下文, 很少用而且不建议使用。

2、执行上下文的特点

  1. 单线程;
  2. 同步执行,从上往下顺序执行;
  3. 全局上下文只有一个,也就是 window 对象;
  4. 函数执行上下文没有数量限制;
  5. 函数只有在调用的时候才会被创建,每调用一次就会产生一个新的执行上下文环境。

3、执行上下文的生命周期

1. 创建阶段

  1. 创建变量对象:首先初始化函数的参数 arguments,提升函数声明和变量声明。
  2. 创建作用域链:作用域链是在变量对象之后创建的。作用域链本身包含变量对象。作用域链用于解析变量。当被要求解析变量时,JavaScript 始终从代码嵌套的最内层开始,如果最内层没有找到变量,就会跳转到上一层父作用域中查找,直到找到该变量。
  3. 确定 this 指向:确定 this 的指向。

2. 执行阶段

  1. 执行变量赋值。
  2. 函数引用。
  3. 执行其他代码。

3. 回收阶段

  1. 执行上下文出栈
  2. 等待虚拟机回收执行上下文

4、js 如何管理多个执行上下文的?

管理多个执行上下文靠的就是执行栈,也被叫做调用栈。

特点:后进先出(LIFO:last-in, first-out)的结构。 作用:存储在代码执行期间的所有执行上下文。

示例:

var a = 1; // 1. 全局上下文环境
function bar (x) {
    console.log('bar')
    var b = 2;
    fn(x + b); // 3. fn上下文环境
}
function fn (c) {
    console.log(c);
}
bar(3); // 2. bar上下文环境

执行上下文

二十九、实现一些特殊函数

1、一次性函数

function once (func) {
  let done;
  return function () {
    if (!done) {
      func.apply(null, arguments)
      done = true
    }
  }
}
const onlyDoOne = once(function() {
  console.log('1')
})
onlyDoOne() // 1
onlyDoOne() // 没有输出,不会再次执行

2、延迟函数(沉睡函数)

function sleep (time) {
    return new Promise(resolve => {
    window.setTimeout(resolve, time)
  })
}
// 调用
sleep(1000).then(res => {
    console.log('延迟')
})
// 调用
async function useSleep () {
    const sleepval = await sleep(1000)
}
useSleep()

3、setTimeout 实现 setInterval

;(() => {
  const list = new Set();
  function myInterval(fn, ms) {
    const ref = {};
    const exec = () => {
      return setTimeout(() => {
        fn.apply(null);
        const timer = exec();
        ref.current = timer;
      }, ms);
    };
    ref.current = exec();
    list.add(ref);
    return ref;
  }
  function myClearInterval(ref) {
    clearTimeout(ref.current);
    list.delete(ref);
  }
  window.myInterval = myInterval;
  window.myClearInterval = myClearInterval;
})()
myInterval(() => {console.log(1)}, 5000)
myClearInterval({current: 1186})

4、前端生成 excel 表格并下载

/**
 * 前端下载表格
 * @param  {[Array]} data      [数据数组]
 * @param  {[String]} tableHead [表头字符串]
 * @return {[undefined]}           
 */
function downExcel (data, tableHead) {
  tableHead = tableHead
  data.forEach(item => {
    for (let i in item) {
      tableHead += `${item[i] + '\t'},`
    }
    tableHead += '\n'
  })
  const url = 'data:text/csv;charset=utf-8,\ufeff' + encodeURIComponent(tableHead);
  //通过创建a标签实现
  const link = document.createElement("a");
  link.href = url;
  //对下载的文件命名
  link.download = "我的EXCEL表格.csv";
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}
// excel 数据
let tableData = [{
  name: '你好啊',
  time: 130000000000,
  pre: '127.130',
  source: '淘宝',
  otherTime: 1571276232000
}]
// excel 头部
let str = `用户名,时间,坐标,来源,授权时间\n`;
// 下载表格执行
downExcel(tableData, str)

三十、什么是原型、原型链?

原型JS 声明构造函数(用来实例化对象的函数)时,会在内存中创建一个对应的对象,这个对象就是原函数的原型。

构造函数默认有一个 prototype 属性,prototype 的值指向函数的原型。同时原型中也有一个 constructor 属性,constructor 的值指向原函数。

通过构造函数实例化出来的对象,并不具有 prototype 属性,其默认有一个 __proto__ 属性,__proto__ 的值指向构造函数的原型对象。在原型对象上添加或修改的属性,在所有实例化出的对象上都可共享。

原型原型链 当在实例化的对象中访问一个属性时,首先会在该对象内部寻找,如找不到,则会向其 __proto__ 指向的原型中寻找,如仍找不到,则继续向原型中 __proto__ 指向的上级原型中寻找,直至找到或 Object.prototype 为止,这种链状过程即为原型链

三十一、实现一个 EventBus

简单实现

class myEventBus {
  constructor(props) {
    this.events = {}
  }
  on (event, fn) {
    const events = this.events
    events[event] ? events[event].push(fn) : (events[event] = [fn])
  }
  emit (event, ...res) {
    this.events[event] && this.events[event].forEach(fn => {
      return fn.apply(this, res)
    })
  }
  remove (event, fn) {
    if (this.events[event]) {
      delete this.events[event]
    }
  }
}

三十二、js 的垃圾回收(GC)

1、V8 内存限制

  • 64 位系统可用 1.4G 内存
  • 32 位系统可用 0.7G 内存

2、V8 内存管理

  • JS 对象都是通过 V8 进行分配管理内存的
  • process.memoryUsage() 返回一个对象,包含了 Node 进程的内存占用信息

3、内存占用结构图

内存占用图

  • var a = {name:‘yuhua’}; 这句代码会做如下几步:
    • 将这句代码放入“代码区域 Code Segment
    • 将变量 a 放入“栈( Stack ):本地变量、指针”
    • {name:‘yuhua’} 放入“ HeapTotal (堆):对象,闭包”
  • 注意:基本数据类型都在栈中,引用类型都在堆中

4、为何限制内存大小

  • 因为 V8 垃圾收集工作原理导致的,1.4G 内存完全一次垃圾收集需要 1s 以上
  • 这个垃圾回收这段时间(暂停时间)成为 Stop The World,在这期间,应用的性能和响应能力都会下降

5、V8 的垃圾回收机制

  • V8 是基于分代的垃圾回收
  • 不同代垃圾回收机制也不一样,采用的算法不一样
  • 按存货的时间分为新生代和老生代

6、分代

  • 年龄小的是新生代,由 From 区域和 To 区域两个区域组成
  • 在 64 位系统里,新生代内存是 32M,From 区域和 To 区域各占 16M
  • 在 32 位系统里,新生代内存是 16M,From 区域和 To 区域各占 8M
  • 年龄大的是老生代,默认情况下:
    • 64 位系统下老生代内存是 1400M
    • 32 位系统下老生代内存是 700M

7、新生代采用 Scavenge 算法

Scavenge 为新生代采用的算法,是一种采用复制的方式实现的垃圾回收算法。

新生代扫描的时候是一种广度优先的扫描策略

它将内存分为 fromto 两个空间。每次 gc,会将 from 空间的存活对象复制到 to 空间。然后两个空间角色对换(又称反转)。

该算法是牺牲空间换时间,所以适合新生代,因为它的对象生存周期较短。

1. 过程

  • 新生代区域一分为二,每个 16M,一个使用,一个空闲
  • 开始垃圾回收的时候,会检查 FROM 区域中的存活对象,如果还活着,拷贝到 TO 空间,所有存活对象拷贝完后,清空(释放) FROM 区域
  • 然后FROM和To区域互换

2. 特点

  • 新生代扫描的时候是一种广度优先的扫描策略
  • 新生代的空间小,存活对象少
  • 当一个对象经理多次的垃圾回收依然存活的时候,生存周期比较差的对象会被移动到老声带,这个移动过程被称为晋升或升级
  • 经历过 5 次以上的回收还存在
  • TO 的空间使用占比超过 25%,或者超大对象
  • 浏览器的 memory 中可以通过拍快照看变量是否被垃圾回收
  • 置为 undefinednull 都能将引用计数减去 1

8、老生代采用 Mark-SweepMark-Compact

1. 基础

  • 老生代垃圾回收策略分为两种
    • mark-sweep 标记清除
      • 标记活着的对象,虽然清楚在标记阶段没有标记的对象,只清理死亡对象 会出现的问题:清除后内存不连续,碎片内存无法分配
    • mark-compact 标记整理
      • 标记死亡后会对对象进行整理,活着的左移,移动完成后清理掉边界外的内存(死亡的对象)
  • 老生代空间大,大部分都是活着的对象,GC 耗时比较长
  • GC 期间无法想听,STOP-THE-WORLD
  • V8 有一个优化方案,增量处理,把一个大暂停换成多个小暂停 INCREMENT-GC
  • 也就是把大暂停分成多个小暂停,每暂停一小段时间,应用程序运行一会,这样垃圾回收和应用程序交替进行,停顿时间可以减少到1/6左右

2. 过程

假设有10个大小的内存,内存占用了6个,

1)Mark-Sweep 模式垃圾回收:
  • 那么会给每个对象做上标记:
A   b   C   d   E   f  空  空 空 空
//对上面每个对象做上标记,大写表示活着,小写表示死了
//这时候,会存在一个问题,就是内存碎片无法使用,因为小写的内存没有跟后面空空空空的内存放在一起,不能使用
  • 这时候小写(死)的都会被干掉,只保留大写(活)的,导致的问题就是内存碎片无法使用
2)Mark-Compact 模式垃圾回收
  • 将活的左移
A C E b d f 空 空 空 空
  • 然后回收死了的区域
A C E 空 空 空 空 空 空 空

9、三种算法的对比

回收算法Mark-SweepMark-CompactScavenge
速度中等最慢最快
空间开销双倍空间(无碎片)
是否移动对象
  • V8 老生代主要用 Mark-Sweep,因为 Mark-Compact 需要移动对象,执行速度不快。空间不够时,才会用 Mark-Compact

三十三、设计模式

1、设计原则:

1. 单一职责原则(SRP

一个对象或方法只做一件事情。

2. 最少知识原则(LKP

应当尽量减少对象之间的交互。

3. 开放-封闭原则(OCP

软件实体(类、模块、函数)等应该是可以 扩展的,但是不可修改

2、策略模式

策略模式是指对一系列的算法定义,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。 优点:

  • 策略模式利用组合、委托等技术和思想,可以避免很多if条件语句
  • 策略模式提供了开放-封闭原则,使代码更容易理解和拓展

示例:

  1. 绩效等级和薪资计算奖金为
  2. 表单验证,通常会涉及到多个字段有效性判断

3、缓存代理模式

缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前的一致,则可以直接返回前面存储的运算结果,提供效率以及节省开销。

缓存代理,就是将前面使用的值缓存下来,后续还有使用的话,就直接拿出来用。

4、工厂模式

工厂模式是用来创建对象的一种最常用的设计模式。我们不暴露创建对象的具体逻辑,而是将将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂。工厂模式根据抽象程度的不同可以分为:简单工厂,工厂方法和抽象工厂。

简单工厂的优点在于,你只需要一个正确的参数,就可以获取到你所需要的对象,而无需知道其创建的具体细节。简单工厂只能作用于创建的对象数量较少,对象的创建逻辑不复杂时使用。

工厂方法模式的本意是将实际创建对象的工作推迟到子类中,工厂方法模式就是将这个大厂拆分出各个小厂,每次添加新的产品让小厂去生产,大厂负责指挥就好了。

抽象工厂模式并不直接生成实例, 而是用于对产品类簇的创建。

5、单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

确保了只有一个实例

  • 因为只有唯一实例,所以节省了系统资源,记住创建和销毁也需要浪费内存资源
  • 避免了对资源的多重占用,比如数据库的连接
  • 资源共享

前端应用场景:

  • 浏览器的 window 对象。在 JavaScript 开发中,对于这种只需要一个的对象,往往使用单例实现。
  • 遮罩层、登陆浮窗等。

6、代理模式

为一个对象提供一个代用品或占位符,以便控制对它的访问。

代理模式主要有三种:保护代理、虚拟代理、缓存代理

7、迭代器模式

迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。

JS 中数组的 map forEach 已经内置了迭代器

8、发布-订阅者模式

也称作观察者模式,定义了对象间的一种一对多的依赖关系,当一个对象的状态发 生改变时,所有依赖于它的对象都将得到通知。

JS中的事件就是经典的发布-订阅模式的实现

9、命令模式

用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系 命令(command)指的是一个执行某些特定事情的指令

三十四、函数&自执行函数

1、自执行函数特点

  1. 函数表达式与函数声明不同,函数名只在该函数内部有效,并且此绑定是常量绑定
  2. 对于一个常量进行赋值,在 strict 模式下会报错,非 strict 模式下静默失败。
  3. IIFE 中的函数是函数表达式,而不是函数声明。

2、函数类型

  1. 函数声明
  2. 函数表达式
  3. 函数构造器创建

1. 函数声明(FD

  1. 有一个特定的名称
  2. 在源码中的位置:要么处于程序级(Program level),要么处于其它函数的主体(FunctionBody)中
  3. 在进入上下文阶段创建
  4. 影响变量对象
  5. 以下面的方式声明
function funName () {}

2. 函数表达式(FE

  1. 在源码中须出现在表达式的位置
  2. 有可选的名称
  3. 不会影响变量对象
  4. 在代码执行阶段创建
// 函数表达式
var foo = function () {} // 匿名函数表达式赋值给变量foo
var foo2 = function _foo2() {} // 外部FE通过变量“foo”来访问——foo(),而在函数内部(如递归调用),有可能使用名称“_foo”。
// 圆括号(分组操作符)内只能是表达式
(function foo() {});
// 在数组初始化器内只能是表达式
[function bar() {}];
// 逗号也只能操作表达式
1, function baz() {};
// !
!function() {}();
(function foo() {})() // 自执行函数 IIFE
(function () {})() // IIFT
var foo = {
  bar: function (x) {
    return x % 2 != 0 ? 'yes' : 'no';
  }(1)
};
foo.bar // 'yes'

3. 函数构造器创建的函数

我们将它与 FDFE 区分开来。其主要特点在于这种函数的[[Scope]]属性仅包含全局对象

var x = 10;
function foo() {
  var x = 20;
  var y = 30;
  var bar = new Function('alert(x); alert(y);');
  bar(); // 10, "y" 未定义
}

3、如何创建一个函数不需要 () 就可以执行

  1. 创建对象
  2. 对象里面表达式定义自执行函数
var foo = {
  bar: function (x) {
    return x % 2 != 0 ? 'yes' : 'no';
  }(1)
};
foo.bar // 'yes'

4、为什么有些要加 () 有些可以不加?

当函数不在表达式的位置的时候,分组操作符圆括号是必须的——也就是手工将函数转化成 FE

如果解析器知道它处理的是 FE,就没必要用圆括号。

5、具名函数表达式

当函数表达式 FE 有一个名称(称为命名函数表达式,缩写为 NFE)时,将会出现一个重要的特点。

从定义(正如我们从上面示例中看到的那样)中我们知道函数表达式不会影响一个上下文的变量对象(那样意味着既不可能通过名称在函数声明之前调用它,也不可能在声明之后调用它)。 但是,FE在递归调用中可以通过名称调用自身。

(function foo(bar) {
  if (bar) {
    return;
  }
  foo(true); // "foo" 是可用的
})();

foo” 储存在什么地方?在 foo 的活动对象中?不是,因为在 foo 中没有定义任何” foo ”。在上下文的父变量对象中创建 foo?也不是,因为按照定义—— FE 不会影响 VO (变量对象)——从外部调用 foo 我们可以实实在在的看到。那么在哪里呢?

当解释器在代码执行阶段遇到命名的 FE 时,在 FE 创建之前,它创建了辅助的特定对象,并添加到当前作用域链的最前端。然后它创建了 FE,此时(正如我们在第四章 作用域链知道的那样)函数获取了[[Scope]] 属性——创建这个函数上下文的作用域链)。此后,FE 的名称添加到特定对象上作为唯一的属性;这个属性的值是引用到 FE 上。最后一步是从父作用域链中移除那个特定的对象。

6、自执行函数示例

// 例一
+function foo(){
foo=10;//我的问题代码
    console.log(foo);//方法自己
}();
console.log(typeof foo);//undefined 观察是否全局污染

// 例二
var b = 10;
(function b() {
   // 内部作用域,会先去查找是有已有变量b的声明,有就直接赋值20,确实有了呀。发现了具名函数 function b(){},拿此b做赋值;
   // IIFE的函数无法进行赋值(内部机制,类似const定义的常量),所以无效。
  // (这里说的“内部机制”,想搞清楚,需要去查阅一些资料,弄明白IIFE在JS引擎的工作方式,堆栈存储IIFE的方式等)
    b = 20;
    console.log(b); // [Function b]
    console.log(window.b); // 10,不是20
})();

// 严格模式 会报错
var b = 10;
(function b() {
  'use strict'
  b = 20;
  console.log(b)
})() // "Uncaught TypeError: Assignment to constant variable."

// 普通函数
function a () {
    a = 1
    console.log(a)
}
a() // 1
a() // a is not a function

三十五、XSS 攻击和 CSRF 攻击

1、XSS 攻击

1. 概念

XSS(Cross Site Scripting):跨域脚本攻击。

2. 原理

不需要你做任何的登录认证,它会通过合法的操作(比如在 url 中输入、在评论框中输入),向你的页面注入脚本(可能是 jshmtl 代码块等)。

3. 防范

  1. 编码;对于用户输入进行编码。
  2. 过滤;移除用户输入和事件相关的属性。(过滤 scriptstyleiframe 等节点)
  3. 校正;使用 DOM Parse 转换,校正不配对的 DOM 标签。
  4. HttpOnly

4. 分类

  • 反射型(非持久):点击链接,执行脚本
  • 存储型(持久):恶意输入保存数据库,其他用户访问,执行脚本
  • 基于 DOM:恶意修改 DOM 结构,基于客户端

2、CSRF 攻击

1. 概念

SRF(Cross-site request forgery):跨站请求伪造。

2. 原理

  1. 登录受信任网站 A,并在本地生成 Cookie。(如果用户没有登录网站 A,那么网站 B 在诱导的时候,请求网站 Aapi 接口时,会提示你登录)。
  2. 在不登出 A 的情况下,访问危险网站 B(其实是利用了网站 A 的漏洞)。

3. 防范

  1. token 验证;
  2. 隐藏令牌;把 token 隐藏在 http 请求的 head 中。
  3. referer 验证;验证页面来源。

3、两者区别

  1. CSRF:需要用户先登录网站 A,获取 cookieXSS:不需要登录。
  2. CSRF:是利用网站 A 本身的漏洞,去请求网站 AapiXSS:是向网站 A 注入 JS 代码,然后执行 JS 里的代码,篡改网站 A 的内容。

三十六、input 输入框输入即请求后端接口,频繁请求之后怎样确定最后一次接口的返回值?

1、后端返回请求值(最简单)

前端请求接口的时候会把 input 输入框中的值传给后端,此时后端返回接口数据时把前端传入的值返回回去,页面渲染时只需要进行判断即可。

2、终止上一次请求

当再次请求的时候把上次的请求终止掉:

  1. ajaxabort()
  2. axios: CancelToken
  3. fetchAbortController

百度用的就是这种取消请求的方式 js:ss1.bdstatic.com/5eN1bjq8AAU…

百度

3. 定义一个全局 ID,接口请求之前自增,然后请求接口闭包保存此值,返回之后进行两者判断。

此种方式就是不用后端返回值,前端存储对应的值信息,进行判断处理

实现

let id = 1
function ajax() {
  ++id
  console.log(id)
  function getData () {
    const newId = id
    const time = Math.random() * 5000 | 0 // 定义一个随机值
    console.log('time', time)
    setTimeout(() => {
      console.log('id newId', id, newId)
      if (id === newId) { // 在此进行数据处理
        console.log('this is true-->', id)
      }
    }, time)
  }
  getData()
}
// click 频繁点击出发函数
document.getElementById('ajaxbtn').onclick = function () {
  ajax()
}

返回结果

三十七、rem

1、定义

rem(font size of the root element)是指相对于根元素的字体大小的单位。 1rem 等于根元素 htmfont-size,即只需要设置根元素的 font-size,其它元素使用 rem 单位时,设置成相应的百分比即可。

2、如何实现

rem(倍数) =  width  / (html的font-size)=>  width = (html的font-size) * rem(倍数)

只要 htmlfont-size 的大小变了,width 就会自动变,所以 rem 是通过动态设置 htmlfont-size 来改变 width 的大小,以达到网页自适应大小的目的

定义公式:rem(倍数) = width / (html的font-size),根据公式我们可以得出: rem(倍数) = 设计稿宽度( imgWidth ) / 你设置的font-size( defalutSize ) rem(倍数) = 网页的实际宽度(screenWidth) / 你需要动态设置的font-size( x ) ,那么得出设置html的font-size的公式为:

<script type="text/javascript">
  (function(w,d) {
  function setSize() {
    var screenWidth = d.documentElement.clientWidth;
    var currentFontSize = screenWidth * 100 / 750;
    d.documentElement.style.fontSize = currentFontSize + 'px';
  }
  w.addEventListener('resize',setSize);
  w.addEventListener('pageShow',setSize)
  w.addEventListener('DOMContentLoaded',setSize)
})(window,document)
</script>
function setHtmlSize(){
  var pageWidth = window.innerWidth;
  if(typeof pageWidth != "number"){ 
    if(document.compatMode == "number"){ 
      pageWidth = document.documentElement.clientWidth;
    }else{ 
      pageWidth = document.body.clientWidth; 
    } 
  } 
  var fontSize = (window.innerWidth * 100) / 750;
  if(fontSize<40){
    fontSize = 40;
  }
  //根据屏幕大小确定根节点字号
  document.getElementsByTagName('html')[0].style.fontSize = fontSize + 'px';
}
function resize(){
  setHtmlSize();
}
if (window.attachEvent) { 
  window.attachEvent("resize", resize); 
} else if (window.addEventListener) { 
  window.addEventListener("resize", resize, false);   
}
setHtmlSize();

3、以 750 宽度来算,1rem = 100pxiphone6/7/8 plus 中设置 width: 6.5rem 元素的宽为多少?

plus 中宽度为 414 所以宽度为 414 / 750 * 6.5 * 100 0.32 rem414 / 750 * 0.32 * 100

三十八、dns-prefetchprefetchpreloaddeferasync

1、dns-prefetch

域名转化为 ip 是一个比较耗时的过程,dns-prefetch 能让浏览器空闲的时候帮你做这件事。尤其大型网站会使用多域名,这时候更加需要 dns 预取。

//来自百度首页
<link rel="dns-prefetch" href="//m.baidu.com">

2、prefetch

prefetch 一般用来预加载可能使用的资源,一般是对用户行为的一种判断,浏览器会在空闲的时候加载 prefetch 的资源。

<link rel="prefetch" href="http://www.example.com/">

3、preload

prefetch 不同,prefecth 通常是加载接下来可能用到的页面资源,而 preload 是加载当前页面要用的脚本、样式、字体、图片等资源。所以 preload 不是空闲时加载,它的优先级更强,并且会占用 http 请求数量。

<link rel='preload' href='style.css' as="style" onload="console.log('style loaded')"

as 值包括

  • script
  • style
  • image
  • media
  • document onload 方法是资源加载完成的回调函数

4、deferasync

//defer
<script defer src="script.js"></script>
//async
<script async src="script.js"></script>

deferasync 都是异步(并行)加载资源,不同点是 async 是加载完立即执行,而 defer 是加载完不执行,等到所有元素解析完再执行,也就是 DOMContentLoaded 事件触发之前。 因为 async 加载的资源是加载完执行,所以它比不能保证顺序,而 defer 会按顺序执行脚本。

三十九、浏览器渲染过程

浏览器渲染过程

1、浏览器渲染过程如下

  • 解析 HTML,生成 DOM
  • 解析 CSS,生成 CSSOM
  • DOM 树和 CSSOM 树结合,生成渲染树(Render Tree)
  • Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
  • Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
  • Display:将像素发送给 GPU,展示在页面上。(这一步其实还有很多内容,比如会在 GPU 将多个合成层合并为同一个层,并展示在页面中。而 css3 硬件加速的原理则是新建合成层)

2、何时触发回流和重绘

1. 回流

  • 添加或删除可见的 DOM 元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
  • 页面一开始渲染的时候(这肯定避免不了)
  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

2. 重绘

  • 回流一定会触发重绘
  • 当页面中元素样式的改变并不影响它在文档流中的位置时(例如:colorbackground-colorvisibility 等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

3、如果避免触发回流和重绘

1. css

  • 避免使用 table 布局。
  • 尽可能在 DOM 树的最末端改变 class
  • 避免设置多层内联样式。
  • 将动画效果应用到 position 属性为 absolutefixed 的元素上
  • 避免使用 CSS 表达式(例如:calc()
  • CSS3 硬件加速(GPU 加速)
    • transform
    • opacity
    • filters
    • Will-change

2. JavaScript

  • 避免频繁操作样式,最好一次性重写 style 属性,或者将样式列表定义为 class 并一次性更改 class 属性,修改 stylecssText 属性或者修改元素的 className 值。
  • 避免频繁操作 DOM,创建一个 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中
  • 也可以先为元素设置 display: none,操作结束后再把它显示出来。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘
  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来
  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
  • 使用 css3 硬件加速,可以让 transformopacityfilters 这些动画不会引起回流重绘 。但是对于动画的其它属性,比如 background-color 这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

4、硬件加速原理

浏览器接收到页面文档后,会将文档中的标记语言解析为 DOM 树。DOM 树和 CSS 结合后形成浏览器构建页面的渲染树。 渲染树中包含了大量的渲染元素,每一个渲染元素会被分到一个图层中,每个图层又会被加载到 GPU 形成渲染纹理,而图层在 GPUtransform 是不会触发 repaint 的,最终这些使用 transform 的图层都会由独立的合成器进程进行处理。

1. 浏览器什么时候会创建一个独立的复合图层呢?

  • 3D 或者 CSS transform
  • <video><canvas> 标签
  • CSS filters
  • 元素覆盖时,比如使用了 z-index 属性

3D2D transform 的区别就在于,浏览器在页面渲染前为 3D 动画创建独立的复合图层,而在运行期间为 2D 动画创建。动画开始时,生成新的复合图层并加载为 GPU 的纹理用于初始化 repaint。然后由 GPU 的复合器操纵整个动画的执行。最后当动画结束时,再次执行 repaint 操作删除复合图层。

2. 使用硬件加速的问题

  • 内存。如果 GPU 加载了大量的纹理,那么很容易就会发生内容问题,这一点在移动端浏览器上尤为明显,所以,一定要牢记不要让页面的每个元素都使用硬件加速。
  • 使用 GPU 渲染会影响字体的抗锯齿效果。这是因为 GPUCPU 具有不同的渲染机制。即使最终硬件加速停止了,文本还是会在动画期间显示得很模糊。

四十、JSBridge

1、什么是 JSBridge

JSBridge 是一种 JS 实现的 Bridge,连接着桥两端的 NativeH5。它在 APP 内方便地让 Native 调用 JSJS 调用 Native ,是双向通信的通道。JSBridge 主要提供了 JS 调用 Native 代码的能力,实现原生功能如查看本地相册、打开摄像头、指纹支付等。

JSBridge

2、H5native 的区别

nameH5Native
稳定性调用系统浏览器内核,稳定性较差使用原生内核,更加稳定
灵活性版本迭代快,上线灵活迭代慢,需要应用商店审核,上线速度受限制
受网速 影响较大较小
流畅度有时加载慢,给用户“卡顿”的感觉加载速度快,更加流畅
用户体验功能受浏览器限制,体验有时较差原生系统 api 丰富,能实现的功能较多,体验较好
可移植性兼容跨平台跨系统,如 PC 与 移动端,iOSAndroid可移植性较低,对于 iOSAndroid 需要维护两套代码

3、JSBridge 的用途

JSBridge 就像其名称中的『Bridge』的意义一样,是 Native 和非 Native 之间的桥梁,它的核心是 构建 Native 和非 Native 间消息通信的通道,而且是 双向通信的通道。 双向通信的通道:

  • JSNative 发送消息 : 调用相关功能、通知 Native 当前 JS 的相关状态等。
  • NativeJS 发送消息 : 回溯调用结果、消息推送、通知 JS 当前 Native 的状态等。

4、JSBridge 流程

H5 ->通过某种方式触发一个 url -> Native捕获到 url,进行分析->原生做处理-> Native 调用 H5JSBridge 对象传递回调。

实现流程

  • 第一步:设计出一个 NativeJS 交互的全局桥对象
  • 第二步: JS 如何调用 Native
  • 第三步: Native 如何得知 api 被调用
  • 第四步:分析 url- 参数和回调的格式
  • 第五步: Native 如何调用 JS
  • 第六步: H5api 方法的注册以及格式

5、JSBridge 的实现原理

  • JavaScript 调用 Native 推荐使用 注入 API 的方式(iOS6 忽略,Android 4.2以下使用 WebViewClientonJsPrompt 方式)。
  • Native 调用 JavaScript 则直接执行拼接好的 JavaScript 代码即可。

React NativeiOS 端举例:JavaScript 运行在 JSCore 中,实际上可以与上面的方式一样,利用注入 API 来实现 JavaScript 调用 Native 功能。不过 React Native 并没有设计成 JavaScript 直接调用 Object-C,而是 为了与 Native 开发里事件响应机制一致,设计成 需要在 Object-C 去调 JavaScript 时才通过返回值触发调用。原理基本一样,只是实现方式不同。

1. NativeJS

1)安卓

native 调用 js 比较简单,只要遵循:”javascript: 方法名(‘参数,需要转为字符串’)”的规则即可。

mWebView.evaluateJavascript("javascript: 方法名('参数,需要转为字符串')", new ValueCallback() {
        @Override public void onReceiveValue(String value) { //这里的value即为对应JS方法的返回值 }
});
2)IOS

Native 通过 stringByEvaluatingJavaScriptFromString 调用 Html 绑定在 window 上的函数。

2. JSNative

1)安卓

Native 中通过 addJavascriptInterface 添加暴露出来的 JS 桥对象,然后再该对象内部声明对应的 API 方法。

private Object getJSBridge(){  
    Object insertObj = new Object(){ @JavascriptInterface public String foo(){ return "foo";  
        } @JavascriptInterface public String foo2(final String param){ return "foo2:" + param;  
        }  
    }; return insertObj;  
}
2)IOS

Native 中通过引入官方提供的 JavaScriptCore 库(iOS7 以上),然后可以将 api 绑定到 JSContext 上(然后 HtmlJS 默认通过 window.top.* 可调用)。

6、JSBridge 接口实现

JSBridge 的接口主要功能有两个: 调用 Native(给 Native 发消息) 和 接被 Native 调用(接收 Native 消息)。

1. 消息都是单向的,那么调用 Native 功能时 Callback 怎么实现的?

JSBridgeCallback ,其实就是 RPC 框架的回调机制。当然也可以用更简单的 JSONP 机制解释:

当发送 JSONP 请求时,url 参数里会有 callback 参数,其值是 当前页面唯一 的,而同时以此参数值为 key 将回调函数存到 window 上,随后,服务器返回 script 中,也会以此参数值作为句柄,调用相应的回调函数。

callback 参数这个 唯一标识 是这个回调逻辑的关键。这样,我们可以参照这个逻辑来实现 JSBridge:用一个自增的唯一 id,来标识并存储回调函数,并把此 id 以参数形式传递给 Native,而 Native 也以此 id 作为回溯的标识。这样,即可实现 Callback 回调逻辑。

(function () {
    var id = 0,
        callbacks = {},
        registerFuncs = {};
    window.JSBridge = {
        // 调用 Native
        invoke: function(bridgeName, callback, data) {
            // 判断环境,获取不同的 nativeBridge
            var thisId = id ++; // 获取唯一 id
            callbacks[thisId] = callback; // 存储 Callback
            nativeBridge.postMessage({
                bridgeName: bridgeName,
                data: data || {},
                callbackId: thisId // 传到 Native 端
            });
        },
        receiveMessage: function(msg) {
            var bridgeName = msg.bridgeName,
                data = msg.data || {},
                callbackId = msg.callbackId, // Native 将 callbackId 原封不动传回
                responstId = msg.responstId;
            // 具体逻辑
            // bridgeName 和 callbackId 不会同时存在
            if (callbackId) {
                if (callbacks[callbackId]) { // 找到相应句柄
                    callbacks[callbackId](msg.data); // 执行调用
                }
            } elseif (bridgeName) {
                if (registerFuncs[bridgeName]) { // 通过 bridgeName 找到句柄
                    var ret = {},
                        flag = false;
                    registerFuncs[bridgeName].forEach(function(callback) => {
                        callback(data, function(r) {
                            flag = true;
                            ret = Object.assign(ret, r);
                        });
                    });
                    if (flag) {
                        nativeBridge.postMessage({ // 回调 Native
                            responstId: responstId,
                            ret: ret
                        });
                    }
                }
            }
        },
        register: function(bridgeName, callback) {
            if (!registerFuncs[bridgeName])  {
                registerFuncs[bridgeName] = [];
            }
            registerFuncs[bridgeName].push(callback); // 存储回调
        }
    };
})();

7、JSBridge 如何引用

1. 由 Native 端进行注入

注入方式和 Native 调用 JavaScript 类似,直接执行桥的全部代码。

优点:桥的版本很容易与 Native 保持一致,Native 端不用对不同版本的 JSBridge 进行兼容;与此同时,

缺点:注入时机不确定,需要实现注入失败后重试的机制,保证注入的成功率,同时 JavaScript 端在调用接口时,需要优先。

2. 由 JavaScript 端引用

直接与 JavaScript 一起执行。

优点JavaScript 端可以确定 JSBridge 的存在,直接调用即可; 缺点:如果桥的实现方式有更改,JSBridge 需要兼容多版本的 Native Bridge 或者 Native Bridge 兼容多版本的 JSBridge

四十一、web worker

1、什么是 web worker?有哪些好处?有哪些问题?

Web Worker 就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。

好处

好处就是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。

问题:

Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。

2、使用 web worker 有哪些限制?

1. 同源限制

分配给 worker 的脚本文件,必须与主线程脚本文件同源。

2. DOM 限制

worker 线程无法读取主线程所在网页的 DOM 对象,无法使用 documentwindowparent 这些对象,可以使用 navigatorlocation 对象。

3. 通信限制

worker 线程和主线程不再同一个上下文环境中,不能直接通信,必须通过消息完成。

4. 脚本限制

worker 线程不能执行 alert 方法和 confirm 方法,但是可以发出 ajax 请求。

5. 文件限制

worker 线程无法读取本地文件,不能打开文件系统,所加载的脚本,必须来自网络,不能是 file:// 文件。

3、worker 线程怎样监听主线程的消息的?如何发送消息的?worker 线程又是如何关闭的?

Worker 线程内部需要有一个监听函数,监听 message 事件。

// 监听
self.addEventListener('message', function (e) {
  // 发送消息
  self.postMessage('You said: ' + e.data);
}, false);

关闭 worker 线程

1)主线程关闭 worker 线程

worker.terminate()

2)worker 线程关闭

self.close()

4、worker 线程如何加载其他脚本?

importScript('scripts.js')
importScript('scripts1.js', 'scripts2.js')

5、主线程和 worker 线程的 API

主线程worker 线程
Worker.onerror:指定 error 事件的监听函数self.nameWorker 的名字
Worker.onmessage:指定 message 事件的监听函数self.onmessage:指定 message 事件的监听函数
Worker.onmessageerror:指定 messageerror 事件的监听函数self.onmessageerror:指定 messageerror 事件的监听函数
Worker.postMessage():向 Worker 线程发送消息self.close():关闭 Worker 线程
Worker.terminate():立即终止 Worker 线程self.postMessage():向产生这个 Worker 线程发送消息
self.importScripts():加载 JS 脚本

四十二、webSocket

1、为什么需要 webSocket?有什么特点?

1. 优势:

  1. 支持双向通信,实时性更强;
  2. 更好的二进制支持;
  3. ws 客户端与服务端数据交换时,数据包头部较小,更好的控制开销;
  4. 支持拓展。

2. 特点:

  1. 建立在 TCP 协议之上,服务器端的实现比较容易。
  2. HTTP 协议有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
  3. 数据格式比较轻量,性能开销小,通信高效。
  4. 可以发送文本,也可以发送二进制数据。
  5. 没有同源限制,客户端可以与任意服务器通信。
  6. 协议标识符是 ws(如果加密,则为 wss),服务器网址就是 URL

ws

2、webSocket 的链接状态?

  • 0 (WebSocket.CONNECTING) 正在链接中
  • 1 (WebSocket.OPEN) 已经链接并且可以通讯
  • 2 (WebSocket.CLOSING) 连接正在关闭
  • 3 (WebSocket.CLOSED) 连接已关闭或者没有链接成功