六万字、42个知识点、超长篇幅助你了解 JS(一)JS 面试大全

1,275 阅读20分钟

此篇文章 Markdown 字数 六万六千 多字,HTML 字数 五万五千 字,全是知识点...

掘金...六万字发不了,只能拆开,拆成三篇文章。

六万字、42个知识点、超长篇幅助你了解 JS(一)JS 面试大全

六万字、42个知识点、超长篇幅助你了解 JS(二)JS 面试大全

六万字、42个知识点、超长篇幅助你了解 JS(三)JS 面试大全

一、什么是编译型语言?什么是解释型语言?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);
        });
    });
}

掘金不能发布六万字文章,所以拆成了三部分

六万字、42个知识点、超长篇幅助你了解 JS(一)JS 面试大全

六万字、42个知识点、超长篇幅助你了解 JS(二)JS 面试大全

六万字、42个知识点、超长篇幅助你了解 JS(三)JS 面试大全