JS基础

ES6有哪些新特性?

主要为以下几部分,详细介绍推荐阅读《ECMAScript 6 教程》

  1. let, const
  2. promise
  3. class
  4. set,map
  5. async await
  6. 箭头函数
  7. symbol

var, let, const 的区别?

  1. var 声明的变量存在变量提升,而let和const不存在变量提升
  2. let 和 const 声明形成块作用域, 必须先定义后使用
  3. 同一作用域下let 和 const 不能声明同名变量, 而 var 可以
  4. 通过 let / const 声明的变量直到它们的定义被执行才初始化。在变量初始化前访问该变量会导致ReferenceError。该变量处在一个自块顶部到初始化处理的”暂时性死区”中。
1
2
3
4
5
6
function do_something(){
console.log(bar); // undefined
console.log(foo); // ReferenceError
var bar = 1;
let foo = 2;
}
  1. const 一旦声明必须赋值, 不能使用null 占位; 声明后不能再修改;如果声明的是复合类型数据,可以修改其属性。
  2. var 声明的变量会作为 window 的一个属性, 而 let 和 const 声明的变量不会

JS有几种基本数据类型?

7种,分别为: Boolean, Null, Undefined, Number, BigInt, String, Symbol。

列举几种引用对象:

  • 普通对象 Object
  • 数组对象 Array
  • 正则对象 RegExp
  • 函数 Function

typeof 能判断哪些类型

typeof用来返回一个值的变量类型, 对于不同类型的变量其返回值如下:

1
2
3
4
5
6
7
8
9
10
typeof undefined === 'undefined'
typeof true === 'boolean'
typeof 78 === 'number'
typeof 'hey' === 'string'
typeof Symbol() === 'symbol'
typeof BigInt(1) === 'bigint'
typeof new String('abc') === 'object'
typeof null === 'object'
typeof function(){} === 'function'
typeof {name: 'Jack'} === 'object'

需注意 typeof 是用来返回值的类型,而不是返回变量的类型,因为JavaScript中的变量是没有类型的

比如: a变量是没有类型的, 但是赋给a变量的值却是有类型的

1
2
3
4
let a = 1;
console.log(typeof a) // 'number'
a = '1'
console.log(typeof a) // 'string'

typeof对于大多数对象都会返回Object, 一个例外是函数, 对于函数会返回function。另一个需要注意的是typeof对于null会返回Object, 这是一个历史悠久的bug, 但是由于无数的网站已经默认了这个bug, 所以现在也无法对其进行修正了。所以使用typeof判断null的方法是:

1
2
var a = null;
(!a && typeof a === "object"); // true

如何使用Object.prototype.toString来判断值的类型,为什么使用它可以判断值的类型?

对于复合类型:

1
2
3
Object.prototype.toString.call({name:'Jack'}) // [object Object]
Object.prototype.toString.call(function(){}) // [object Function]
Object.prototype.toString.call(/name/) // [object RegExp]

而对于基本类型:

1
2
3
Object.prototype.toString.call('abc') // [object String]
Object.prototype.toString.call(12) // [object Number]
Object.prototype.toString.call(true) // [object Boolean]

基本类型值是没有构造函数的,为什么也能返回构造函数名呢?这是因为在toString被调用时JavaScript将基本类型值转换成了包装类型。

而对于null 和 undefined:

1
2
Object.prototype.toString.call( null );            // "[object Null]"
Object.prototype.toString.call( undefined ); // "[object Undefined]"

虽然JavaScript中没有 NullUndefined构造器, 但是JavaScript也为我们处理了这两种情况。

说说原型和原型链?

原型对象是在创建普通函数或构造函数时, 解析器都会向函数中添加一个属性prototype,它指向看一个对象,称作原型对象。当我们使用构造函数实例化多个对象时,原型对象相当于一块公共的区域,实例化的对象可以具有隐含的 **proto **属性,指向了原型对象的内存地址,进而可以访问到原型对象。

而原型对象也是一个对象,它同样具有属性prototype指向原型对象的原型对象,当构造函数中不存在实例化对象的某个属性时,它会在它的原型对象中寻找该属性,如果没有该属性,则会在原型对象的原型对象中寻找,这样就形成了一个原型链,直到找到该属性,否则返回null

需要注意几个小细节:

1
2
3
4
5
6
Array instanceof Function // true
Object instanceof Function // true
Function instanceof Function // true
Function instanceof Object // true
// 前 3 个因为 Array,Function,Object 都是构造函数,他们的原型都是
// Function.prototype,而所有的对象最终都指向 Object,所以第 4 个成立,如有问题欢迎讨论

箭头函数和普通函数的区别?

1.箭头函数是匿名函数,不能作为构造函数,不能使用new关键字

1
2
3
4
5
6
7
8
let a = () => {
console.log(111)
}
a();

let fn = new a()
VM325:1 Uncaught TypeError: a is not a constructor
at <anonymous>:1:10

2.箭头函数不绑定 arguments ,取而代之用 rest 参数解决

1
2
3
4
5
6
7
8
function A(a) { console.log(arguments) }
A(2, 'zzm', 'mzm')
Arguments(3) [2, 'zzm', 'mzm', callee: f, Symbol(Symbol.iterator):f]

let B = (b) => {
console.log(arguments);
}
B(2, 92, 32, 32); // Uncaught ReferenceError: arguments is not defined

3.this的作用域不同, 箭头函数不绑定this, 会捕获其所在的上下文this值作为自己的this指向

1
2
3
4
5
6
7
8
9
10
11
12
13
var obj = {
a: 10,
b: () => {
console.log(this.a); // undefined
console.log(this); // window {postMessage: f, blur: f, close: f, frames: Window,...}
},
c: function() {
console.log(this.a); // 10
console.log(this); // {a: 10, f: f, c: f}
}
}
obj.b()
obj.c()

4.箭头函数没有原型属性

1
2
3
4
5
6
7
8
9
10
var a = () => {
return 1;
}

function b() {
return 2;
}

console.log(a.prototype); // undefined
console.log(b.prototype); // { consttructor: f }

5.箭头函数不能当做 Generator 函数, 不能使用 yield 关键字

数组方法,迭代方法:forEach, map

forEach()

forEach()**方法对数组的每一个元素执行一次回调函数。除了抛出异常外,无法终止或者跳出forEach()循环。如果需要终止或者跳出循环,forEach()** 方法不是应当使用的工具。

1
2
3
4
5
const array1 = ['a', 'b', 'c'];
array1.forEach(element => console.log(element));
// "a"
// "b"
// "c"

map()

map()**方法会返回一个新数组**,数组内元素为原始数组元素经过函数处理后的值。与forEach()**执行的回调函数不同的是,**map()**内的回调函数需要有一个返回值**作为返回新数组的元素。

1
2
3
const array2 = ['a', 'b', 'c'];
array2.map(element => element + '1');
// ['a1', 'b1', 'c1']

判断一个对象是否为数组,有几种判断方法?

可总结为以下5种:

1
2
3
4
5
6
let arr = []
arr instanceof Array // 1. 使用 instanceof
arr.__proto__ === Array.prototype // 2. 使用 __proto__
arr.constructor === Array // 3. 使用 constructor ,以上三种主要是通过原型来判断的
Object.prototype.toString.call(arr) === '[object Array]' // 4. 通过 object 类型的副属性 class 来判断,其中 函数的 class 是 Function
Array.isArray(arr) // es6 新增语法

何时使用 === ,何时使用 == ?

除了 **== null**以外,其余一律使用 **===**。下面是一些使用场景:

1
2
3
4
5
6
7
8
9
10
11
100 == '100' // true
0 == '' // true
0 == false // true
false == '' // true
null == undefined // true
// 除了 == null 之外,其他一律用 ===,例如
const obj = {x: 100}
if (obj.a == null) {
// 相当于
// if (obj.a === null || obj.a === undefined) {}
}

值类型与引用类型的区别?

  • 存储位置不同: 值类型存储在 **栈** 内存中, 引用类型存储在 **堆** 内存中
  • 值类型变量的直接赋值是**深拷贝**,在栈内存中新开一块空间来存储值;而引用类型的变量赋值是**浅拷贝**,只传递引用的地址
  • 比较时, 值类型是**值**的比较, 而引用类型是**地址**的比较。 对于引用类型来说,即使值相同,如果在内存中的地址不同,这两个对象仍然是不相等的。

JS继承有哪些方式?

  • 基于原型链的继承(委托关联)
  • 使用 **class, extends, constructor, static****super** 关键字, 只是语法糖, 本质还是基于原型

JS错误捕获机制

  • **throw**: 手动中断程序执行,抛出一个错误
  • **try ... catch**: 对错误进行处理,选择是否往下执行。只有错误可预知时才用, 不可预知错误时使用都是不负责任的写法
  • **finally**:不管是否出现错误,都必须执行最后的语句

什么是闭包?

函数当作一个普通的变量传递, 使得函数在运行时可能会看起来已经脱离了原来的词法作用域。但是由于函数的作用域早就在词法分析时就确定了,所以函数无论在哪里执行,都会记住被定义时的作用域。这种现象就叫作闭包。

1
2
3
4
5
6
7
8
9
function foo() {
const a = 2;
return function bar() {
console.log(a)
}
}

const func = foo()
func() // 打印出 2

当函数bar执行时,很明显其早已脱离了原来的作用域,但是其仍然打印出变量a的值,这就说明它一直记住了它在被定义时的作用域。

综上所述,闭包是**词法作用域****函数**相互所用时自然而然产生的现象。

闭包的使用?

主要是自由变量的查找, 是在 函数定义 的地方,向上级作用域查找,而不是在执行的地方。

1.函数作为参数被传递:

1
2
3
4
5
6
7
8
9
10
// 函数作为参数
function print(fn) {
let a = 200;
fn();
}
let a = 100;
function fn() {
console.log(a)
}
print(fn) // 100,因为自由变量在函数定义的地方向上查找

2.函数作为返回值被返回:

1
2
3
4
5
6
7
8
9
10
// 函数作为返回值
function create() {
let a = 100;
return function() {
console.log(a)
}
}
let fn = create()
let a = 200
fn() // 100

手撕代码

(1)实现深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function deepClone(obj = {}) {
if (typeof obj !== 'object' || obj == null) {
// obj 是 null 或者不是对象和数组,直接返回
return obj;
}

let res;
if (obj instanceof Array) {
res = [];
} else {
res = {};
}

for (let key in obj) {
// 判断自身中是否包含指定属性
if (obj.hasOwnProperty(key)) {
res[key] = deepClone(obj[key]);
}
}
return res;
}

(2)手写bind方法

bind方法的作用?官方说法如下:

bind() 方法创建一个新的函数,在bind()被调用时,这个新函数的this被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数, 供调用时使用

使用场景如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const module = {
x: 42,
getX: function() {
return this.x;
}
};

const unboundGetX = module.getX;
console.log(unboundGetX()); // The function gets invoked at the global scope
// expected output: undefined

const boundGetX = unboundGetX.bind(module);
console.log(boundGetX());
// expected output: 42

模拟实现bind():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 模拟 bind
Function.prototype.bindMock = function() {
// 将参数拆解为数组
const args = Array.prototype.slice.call(arguments) // 变成数组

// 获取 this(数组第一项)
const t = args.shift()

// fn1.bind(...) 中的 fn1
const self = this

// 返回一个函数
return function() {
return self.apply(t, args)
}
}

// 测试
function fn1(a, b, c) {
console.log('this', this)
console.log(a, b, c)
return 'this is fn1'
}
const fn2 = fn1.bind1({x: 100}, 10, 20, 30)
const res = fn2()
console.log(res)

(3)手写promise加载一张图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function loadImg(src) {
const p = new Promise(
(resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
const err = new Error('图片加载失败 ${src}')
reject(err)
}
img.src = src
}
)
return p
}

const url = 'http://xxxxxxxx'
loadImg(url).then(img => {
console.log(img.width)
return img
}).then(img => {
console.log(img.height)
}).catch(ex => console.error(ex))

(4)防抖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function debounce(fn, delay=500){
// timer 写在闭包中,因此防抖也是闭包的一个应用
let timer = null;
return function() {
if(timer)
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, arguments)
timer = null
}, delay)
}
}

// 验证
input1.addEventListener('keyup', debounce(() => {
console.log(input1.value)
}), 600)

(5)节流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 节流
function throttle(fn, delay = 100) {
let timer = null
return function () {
if(timer)
return;
timer = setTimeout(() => {
fn.apply(this, arguments)
timer = null
}, delay)
}
}

div1.addEventListener(('drag', throttle(function (e) {
console.log(e.offsetX, e.offsetY)
})))

JS异步

(1)聊聊Promise?

Promise有三种状态:

  • pending, resolved, rejected
  • pending -> resolved 或 pending -> rejected

状态的表现

  • pending状态,不会触发then和catch
  • resolved状态,会触发then 回调函数
  • rejected状态, 会触发 catch 回调函数
  • then 正常返回resolved, 若有报错则返回rejected
  • catch 正常返回 resolved,有报错则返回rejected

(2)async/await 有什么区别?底层原理是什么?

解决了 **callback hell** 问题,是一个语法糖,promise, then , catch 是链式调用,但也是基于回调函数。它们与Promise的关系如下:

  • 执行 async 函数, 返回的是 Promise 对象
  • await 相当于 Promise 的then
  • try…catch 可以捕获异常,代替了Promise 的catch

底层原理:用Promise 控制 generator 函数的迭代器调用。

(3)async/await、Promise 场景题

1
2
3
4
5
6
7
8
9
10
// catch 正常返回 resolved,里面有报错返回 rejected
const p3 = Promise.reject('my error').catch(err => {
console.error(err)
})
console.log('p3', p3) // resolved !!!! 注意

const p4 = Promise.reject('my error').catch(err => {
throw new Error('catch err')
})
console.log('p4', p4) // rejected
1
2
3
4
5
6
7
Promise.resolve().then(() => {
console.log(1) // 1. 1
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3) // 2. 3
})
1
2
3
4
5
6
7
8
Promise.resolve().then(() => {
console.log(1) // 1. 1
throw new Error('error1')
}).catch(() => {
console.log(2) // 2. 2
}).then(() => {
console.log(3) // 3. 3
})
1
2
3
4
5
6
7
8
Promise.resolve().then(() => {
console.log(1) // 1. 1
throw new Error('error1')
}).catch(() => {
console.log(2) // 2. 2
}).catch(() => {
console.log(3)
})
1
2
3
4
5
6
7
async function fn() {
return 100
}
(async function () {
const a = fn() // Promise,执行 async 返回一个 Promise
const b = await fn() // 100,await 相当于 Promise.then(100),故返回 100
})
1
2
3
4
5
6
7
8
9
10
(async function () {
console.log('start') // 1. start
const a = await 100
console.log('a', a) // 2. a 100
const b = await Promise.resolve(200)
console.log('b', b) // 3. b 200
const c = await Promise.reject(300)
console.log('c', c) // 报错,后面的都不打印
console.log('end')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
async function async1() {
console.log('async start') // 2
await async2()
console.log('async1 end') // 6
}

async function async2 () {
console.log('async2') // 3
}

console.log('script start') // 1

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

async1()

new Promise (function (resolve) {
console.log('promise1') // 4
resolve()
}).then(function () {
console.log('promise2') // 7
})

console.log('script end') // 5

(4)什么是 Event Loop ?

事件循环可以理解为我们编写的JavaScript和浏览器或者Node之间的一个**桥梁**

  1. 首先JavaScript引擎会执行一个宏任务, 注意这个宏任务一般是指主干代码本身**main script**,也就是目前**script**内的的同步代码(从上往下)
  2. 执行过程中如果遇到微任务,就把它添加至微任务队列**microtask queue**
  3. 宏任务队列**macrotask queue**中的 **main script**执行后,立即执行当前微任务队列中的微任务,直到微任务队列被清空
  4. 微任务执行完成后,开始执行下一个宏任务
  5. 如此循环往复,直到宏任务与微任务都清空

注意点

  • main script中的代码优先执行(编写的顶层script代码)
  • 在执行任何一个宏任务之前(不是队列,是一个宏任务),都会先查看微任务队列中是否有任务需要执行
    1. 即宏任务执行之前,必须保证微任务队列是空的
    2. 如果不为空,那么久优先执行微任务队列中的任务(回调)

(5)宏任务和微任务区别?

微任务执行要比宏任务执行要 **早**~~因为宏任务在 **DOM 渲染后** 触发,微任务在 **DOM 渲染前** 触发。

浏览器中事件循环

  • 宏任务(macrotask queue):ajax、setTimeout、setInterval、DOM监听、UI Rendering等
  • 微任务(microtask queue):Promise的then回调、 Mutation Observer API、queueMicrotask()等
  • 执行顺序:main script > 微任务 > 宏任务

本质原因

  • 微任务是ES6语法规定的
  • 宏任务是浏览器规定的

Node.js中事件循环

  • 宏任务:timers 、 IO 、setImmediate 、 close
  • 微任务:nextTick、other microtask( then回调、queueMicrotask)
  • 执行顺序:main script > nextTick > other microtask queue > timers (setTimeout、setIntetval) > IO (poll queue) > immediate (check queue) > close queue