JS基础
ES6有哪些新特性?
主要为以下几部分,详细介绍推荐阅读《ECMAScript 6 教程》
- let, const
- promise
- class
- set,map
- async await
- 箭头函数
- symbol
var, let, const 的区别?
- var 声明的变量存在变量提升,而let和const不存在变量提升
- let 和 const 声明形成块作用域, 必须先定义后使用
- 同一作用域下let 和 const 不能声明同名变量, 而 var 可以
- 通过 let / const 声明的变量直到它们的定义被执行才初始化。在变量初始化前访问该变量会导致ReferenceError。该变量处在一个自块顶部到初始化处理的”暂时性死区”中。
1 | function do_something(){ |
- const 一旦声明必须赋值, 不能使用null 占位; 声明后不能再修改;如果声明的是复合类型数据,可以修改其属性。
- var 声明的变量会作为 window 的一个属性, 而 let 和 const 声明的变量不会
JS有几种基本数据类型?
7种,分别为: Boolean, Null, Undefined, Number, BigInt, String, Symbol。
列举几种引用对象:
- 普通对象 Object
- 数组对象 Array
- 正则对象 RegExp
- 函数 Function
typeof 能判断哪些类型
typeof用来返回一个值的变量类型, 对于不同类型的变量其返回值如下:
1 | typeof undefined === 'undefined' |
需注意 typeof 是用来返回值的类型,而不是返回变量的类型,因为JavaScript中的变量是没有类型的
比如: a变量是没有类型的, 但是赋给a变量的值却是有类型的
1 | let a = 1; |
typeof对于大多数对象都会返回Object, 一个例外是函数, 对于函数会返回function。另一个需要注意的是typeof对于null会返回Object, 这是一个历史悠久的bug, 但是由于无数的网站已经默认了这个bug, 所以现在也无法对其进行修正了。所以使用typeof判断null的方法是:
1 | var a = null; |
如何使用Object.prototype.toString来判断值的类型,为什么使用它可以判断值的类型?
对于复合类型:
1 | Object.prototype.toString.call({name:'Jack'}) // [object Object] |
而对于基本类型:
1 | Object.prototype.toString.call('abc') // [object String] |
基本类型值是没有构造函数的,为什么也能返回构造函数名呢?这是因为在toString被调用时JavaScript将基本类型值转换成了包装类型。
而对于null 和 undefined:
1 | Object.prototype.toString.call( null ); // "[object Null]" |
虽然JavaScript中没有 Null 和 Undefined构造器, 但是JavaScript也为我们处理了这两种情况。
说说原型和原型链?
原型对象是在创建普通函数或构造函数时, 解析器都会向函数中添加一个属性prototype,它指向看一个对象,称作原型对象。当我们使用构造函数实例化多个对象时,原型对象相当于一块公共的区域,实例化的对象可以具有隐含的 **proto **属性,指向了原型对象的内存地址,进而可以访问到原型对象。
而原型对象也是一个对象,它同样具有属性prototype指向原型对象的原型对象,当构造函数中不存在实例化对象的某个属性时,它会在它的原型对象中寻找该属性,如果没有该属性,则会在原型对象的原型对象中寻找,这样就形成了一个原型链,直到找到该属性,否则返回null。
需要注意几个小细节:
1 | Array instanceof Function // true |
箭头函数和普通函数的区别?
1.箭头函数是匿名函数,不能作为构造函数,不能使用new关键字
1 | let a = () => { |
2.箭头函数不绑定 arguments ,取而代之用 rest 参数解决
1 | function A(a) { console.log(arguments) } |
3.this的作用域不同, 箭头函数不绑定this, 会捕获其所在的上下文this值作为自己的this指向
1 | var obj = { |
4.箭头函数没有原型属性
1 | var a = () => { |
5.箭头函数不能当做 Generator 函数, 不能使用 yield 关键字
数组方法,迭代方法:forEach, map
forEach()
forEach()**方法对数组的每一个元素执行一次回调函数。除了抛出异常外,无法终止或者跳出forEach()循环。如果需要终止或者跳出循环,forEach()** 方法不是应当使用的工具。
1 | const array1 = ['a', 'b', 'c']; |
map()
map()**方法会返回一个新数组**,数组内元素为原始数组元素经过函数处理后的值。与forEach()**执行的回调函数不同的是,**map()**内的回调函数需要有一个返回值**作为返回新数组的元素。
1 | const array2 = ['a', 'b', 'c']; |
判断一个对象是否为数组,有几种判断方法?
可总结为以下5种:
1 | let arr = [] |
何时使用 === ,何时使用 == ?
除了 **== null**
以外,其余一律使用 **===**
。下面是一些使用场景:
1 | 100 == '100' // true |
值类型与引用类型的区别?
- 存储位置不同: 值类型存储在
**栈**
内存中, 引用类型存储在**堆**
内存中 - 值类型变量的直接赋值是
**深拷贝**
,在栈内存中新开一块空间来存储值;而引用类型的变量赋值是**浅拷贝**
,只传递引用的地址 - 比较时, 值类型是
**值**
的比较, 而引用类型是**地址**
的比较。 对于引用类型来说,即使值相同,如果在内存中的地址不同,这两个对象仍然是不相等的。
JS继承有哪些方式?
- 基于原型链的继承(
委托关联
) - 使用
**class, extends, constructor, static**
和**super**
关键字, 只是语法糖, 本质还是基于原型
JS错误捕获机制
**throw**
: 手动中断程序执行,抛出一个错误**try ... catch**
: 对错误进行处理,选择是否往下执行。只有错误可预知时才用, 不可预知错误时使用都是不负责任的写法**finally**
:不管是否出现错误,都必须执行最后的语句
什么是闭包?
将函数当作一个普通的变量传递, 使得函数在运行时可能会看起来已经脱离了原来的词法作用域。但是由于函数的作用域早就在词法分析时就确定了,所以函数无论在哪里执行,都会记住被定义时的作用域。这种现象就叫作闭包。
1 | function foo() { |
当函数bar执行时,很明显其早已脱离了原来的作用域,但是其仍然打印出变量a的值,这就说明它一直记住了它在被定义时的作用域。
综上所述,闭包是**词法作用域**
和**函数**
相互所用时自然而然产生的现象。
闭包的使用?
主要是自由变量的查找, 是在 函数定义 的地方,向上级作用域查找,而不是在执行的地方。
1.函数作为参数被传递:
1 | // 函数作为参数 |
2.函数作为返回值被返回:
1 | // 函数作为返回值 |
手撕代码
(1)实现深拷贝
1 | function deepClone(obj = {}) { |
(2)手写bind方法
bind方法的作用?官方说法如下:
bind() 方法创建一个新的函数,在bind()被调用时,这个新函数的this被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数, 供调用时使用
使用场景如下:
1 | const module = { |
模拟实现bind():
1 | // 模拟 bind |
(3)手写promise加载一张图片
1 | function loadImg(src) { |
(4)防抖
1 | function debounce(fn, delay=500){ |
(5)节流
1 | // 节流 |
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 | // catch 正常返回 resolved,里面有报错返回 rejected |
1 | Promise.resolve().then(() => { |
1 | Promise.resolve().then(() => { |
1 | Promise.resolve().then(() => { |
1 | async function fn() { |
1 | (async function () { |
1 | async function async1() { |
(4)什么是 Event Loop ?
事件循环可以理解为我们编写的JavaScript和浏览器或者Node之间的一个**桥梁**
。
- 首先JavaScript引擎会执行一个宏任务, 注意这个宏任务一般是指主干代码本身
**main script**
,也就是目前**script**
内的的同步代码(从上往下) - 执行过程中如果遇到微任务,就把它添加至微任务队列
**microtask queue**
中 - 宏任务队列
**macrotask queue**
中的**main script**
执行后,立即执行当前微任务队列中的微任务,直到微任务队列被清空 - 微任务执行完成后,开始执行下一个宏任务
- 如此循环往复,直到宏任务与微任务都清空
注意点:
- main script中的代码优先执行(编写的顶层script代码)
- 在执行任何一个宏任务之前(不是队列,是一个宏任务),都会先查看微任务队列中是否有任务需要执行
- 即宏任务执行之前,必须保证
微任务队列是空的
- 如果不为空,那么久
优先执行微任务
队列中的任务(回调)
(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