JS高频面试题
apply、call、bind 的区别
- 三者都可以改变函数的 this 对象指向
- 三者第一个参数都是 this 要指向的对象,如果如果没有这个参数或参数为 undefined 或 null,则默认指向全局 window
- 三者都可以传参,但是 apply 是数组,而 call 是参数列表,且 apply 和 call 是一次性传入参数,而 bind 可以分为多次传入
- bind 是返回绑定 this 之后的函数,需要手动执行函数,apply、call 则是立即执行
new 操作符的实现原理
new 操作符的执⾏过程:
(1)⾸先创建了⼀个新的空对象
(2)设置原型,将对象的原型设置为函数的 prototype 对象。
(3)让函数的 this 指向这个对象,执⾏构造函数的代码(为这个新对象添加属性)
(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引⽤类型,就返回这个引⽤类
型的对象。
function objectFactory() { |
map()和 foreach()的区别
简洁回答
都是⽤来遍历数组的,两者区别如下:
- forEach()⽅法会针对每⼀个元素执⾏提供的函数,对数据的操作会改变原数组,该⽅法没有返回
值;
- map()⽅法不会改变原数组的值,返回⼀个新数组,新数组中的值为原数组调⽤函数处理之后的
值;
相同点
- 都是循环遍历数组的每一项
- 都相当于封装好的单层 for 循环,三个值都相同
- 每次执行匿名函数都支持三个参数,参数分别为 item(当前每一项)、index(索引值)、arr(原数组)
- 匿名函数中的 this 都是指向 window
- 只能遍历数组
不同点
- map()会分配内存空间存储新数组并有返回值,forEach()没有返回值
- forEach()允许 calllback 更改原始数组的元素,map()返回新的数组,map()不会对空数组进行检测
- forEach()遍历通常都是直接引入当前遍历数组的内存地址,生成的数组的值发生变化,当前遍历的数组对应的值也会发生变化。
- map 遍历后的数组通常会生成一个新的数组,新数组的值发生变化,当前遍历的数组值不会变化。
- map 的速度大于 forEach
使用场景
- forEach()适用于你并不打算改变数据的时候
- map()适用于你要改变数据的时候。不仅在于它更快,而且返回一个新数组。(因此可以使用复合(composition)(map(),filter(),reduce()等组合使用))
性能上来说,for>forEach>map。
Symbol
- 什么是 Symbol?
Symbol 是 ES6 新增的一种数据类型,被划分为基本数据类型。不能用 new
。
- 基本数据类型:字符串、数值、布尔、undefined、null、Symbol。
- 引用数据类型:Object
- 作用
用来表示一个独一无二的值。
- 格式:
let xxx = Symbol('标识字符串')
- 为什么需要 Symbol?
为了避免第三方框框架的同名属性被覆盖。
在企业开发中,如果需要对一些第三方的插件、框架进行自定义时,可能会因为添加了同名的属性或方法,将框架中原有的属性或方法覆盖掉,为了避免这种情况的发生,框架的作者告诉我们就可以使用 Symbol 作为属性或方法的名称。
- 如果区分 Symbol?
在通过 Symbol 生成一个独一无二的值时,可以设置一个标记
这个标记仅仅用于区分,没有其他任何意义。
- 如果特殊情况需要读取这个标记,
- Symbol 类型可以转化为 boolean 或字符串,转化为字符串时前面会加上 Symbol(wxy),不方便
- 可以直接通过 description 属性获取 Symbol 函数的字符串标识参数
- 使用 Symbol 类型作为属性名
对象的属性要么是字符串,要么是 Symbol 类型
- 默认是字符串,所以不加
""
也可以;如果需要类型为 Symbol,需要使用[]
。 - 不能用
.
来访问,因为点运算符后面总是字符串。 - Symbol 值作为属性名时,该属性还是公开属性,不是私有属性。
- 例子:
//后面的括号可以给symbol做上标记便于识别 |
{
[Symbol(name)]: ‘lnj’,
[Symbol(say)]: [Function: [say]],
[Symbol(name)]: ‘it666’
}
[ Symbol(name), Symbol(say), Symbol(name) ]
没有覆盖原来的 name,因为都是独一无二的,那么就默认创建一个 name 的属性。
注意点
- Symbol 是基本数据类型,不能加 new
- 后面括号可以传入一个字符串,只是一个标记,方便阅读,没有任何意义。
- 类型转化的时候,不可转化为数值,只能转化为字符串和布尔值。
console.log(String(name)); |
- 不能做任何运算
let name = Symbol("name"); |
- Symbol 生成的值作为属性或方法的时候,一定要保存下来,否则后续无法使用。
let name = Symbol("name"); |
应该改为如下:
let name = Symbol("name"); |
[ Symbol(name) ]
lnj1
- for 循环遍历对象时无法遍历出 Symbol 的属性和方法
let name = Symbol("name"); |
Symbol 的应用
- 在企业开发中,如果需要对一些第三方的插件、框架进行自定义时,可能会因为添加了同名的属性或方法,将框架中原有的属性或方法覆盖掉,为了避免这种情况的发生,框架的作者告诉我们就可以使用 Symbol 作为属性或方法的名称。
- 消除魔术字符串
魔术字符串:在代码中多次出现,与代码形成强耦合的某一个具体的字符串或数值。风格良好的代码应该尽量消除魔术字符串,改由含义清晰的变量代替。‘
const gender = { |
- 为对象定义一些非私有的、但又希望只用于内部的方法。
由于以 Symbol 值作为键名,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。
注意:Symbol 并不能实现真正的私有变量的效果,只是不能通过常规的遍历方法拿到 Symbol 类型的属性而已。
对象的遍历方法:
- for(let xx in obj):i 代表 key
- for(let xx of obj):不是自带的
- Object.keys(obj):返回包含的 key 的数组
- Object.values(obj):返回包含 value 的数组
- Object.getOwnPropertyNames():返回包含 key 的数组
上述的所有方法都遍历不到 Symbol 类型的(注意:是遍历时取不到 Symbol,并不是我们访问不到对象的 Symbol 类型)
可以遍历到 Symbol 的方法:
- Object.getOwnPropertySymbols():返回对象中只包含 Symbol 类型 key 的数组
- Reflect.ownKeys():返回对象中所有类型 key 的数组(包含 Symbol)
let _password = Symbol("password"); |
输出如下:
Symbol 自带的方法
1. Symbol.for()
重新使用同一个 Symbol 值。
接收一个字符串作为参数,搜索是否有以该参数作为名称的 Symbol 值,如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。
let s1 = Symbol.for("foo"); |
2. Symbol.keyFor()
返回一个已登记的 Symbol 类型值的key
。
由于Symbol()
写法没有登记机制,所以每次调用都会返回一个不同的值。
let s1 = Symbol.for("foo"); |
Map 和 Object 区别
Map | Object | |
---|---|---|
意外的键 | Map 默认情况不包含任何键,只包含显式插入的键 | Object 有一个原型,原型链上的键名有可能和自己在对象上设置的键名有冲突 |
键的类型 | Map 的键可以是任意值,包括函数、对象或任意基本类型 | Object 的键必须是String 或 Symbol |
键的顺序 | Map 中的key是有序的。因此,当迭代的时候,Map 对象以插入的顺序返回键值 | Object 的键是无序的 |
Size | Map 的键值对个数可以轻易地通过size属性获取 | Object 建值对个数只能手动计算 |
迭代 | Map 是 iterable 的,所以可以直接被迭代 | 迭代 Object 需要以某种方式获取它的键然后才能迭代 |
性能 | 在频繁增删键值对的场景下表现更好 | 在频繁添加和删除键值对的场景下未做出优化 |
Map 和 WeakMap 区别
Map
map 本质上是键值对的集合,但是普通 Object 的中的键值只能是字符串或 Symbol。而 ES6 提供的 Map 数据结构类似于对象,但是它的键不限制范围,可以是任意类型,是一种更加完善的 Hash 结构。如果 Map 的键是一个原始数据类型,只要两个键严格相同,就视为是同一个键。
实际上 Map 是一个数组,它的每一个数据也都是一个数组,其形式如下:
const map = [ |
Map 数据结构有以下操作方法:
- size:
map.size()
返回 Map 结构的成员总数 - set(key,value):设置键名 key 对应的键值 value,然后返回整个 Map 结构,如果 key 已经有值,则键值会被更新,否则就生成该键。(因为返回的是当前 Map 对象,所以可以链式调用)
- get(key):该方法读取 key 对应的键值,如果找不到 key,返回 undefined
- has(key):该方法返回一个布尔值,表示某个键是否在当前 Map 对象中。
- delete(key):该方法删除某个 key,返回 true,如果删除失败,返回 false。
- clear():map.clear() 清除所有成员,没有返回值
Map 结构原生提供是三个遍历器生成函数和一个遍历方法
- keys():返回键名的遍历器
- values():返回键值的遍历器
- entries():返回所有成员的遍历器
- forEach():遍历 Map 的所有成员
const map = new Map([ |
WeakMap
WeakMap 对象也是一组键值对的集合,其中的键是弱引用。**其键必须是对象,**原始数据类型不能作为 key 值,而值可以是任意的。
该对象也有以下几种方法:
- set(key,value):设置键名 key 对应的键值 value,然后返回整个 Map 结构,如果 key 已经有值,则键值会被更新,否则就新生成该键。(因为返回的是当前 Map 对象,所以可以链式调用)
- get(key):读取 key 对应的键值,如果找不到 key,返回 undefined。
- has(key):返回一个布尔值,表示某个键是否在当前 Map 对象中。
- delete(key):删除某个键,返回 true,如果删除失败,返回 false。
其 clear() 方法已经被弃用,所以可以通过创建一个空的 WeakMap 并替换原对象来实现清除。
WeakMap 的设计目的:
- 有时想在某个对象上存放一些数据,但是这会形成对于这个对象的引用。一旦不需要这两个对象,就必须手动删除这个引用,否则垃圾回收机制就不会释放对象占用的内存。
- 而 WeakMap 的键名所引用的对象都是弱引用, 即**垃圾回收机制不将该引用考虑在内。**因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。即一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
总结
- Map 数据结构,类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以作为键。
- WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合。但是 WeakMap只接受对象作为键名(null 除外),不接受其他类型的值作为键名。而且 WeakMap 的键名所指向的对象是弱引用,不计入垃圾回收机制。
JS 内置对象
全局的对象(global objects)或称标准内置对象。即全局作用域中的对象,全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。
标准内置对象的分类
- 值属性:这些全局属性返回一个简单值,这些值没有自己的属性和方法。
如:Infinity、NaN、undefined、null 字面量。
- 函数属性:全局属性可以直接调用,不需要再调用时指定所属对象,执行结束后会将结果直接返回给调用者。
如:eval()、parseFloat()、parseInt()等。
- 基本对象:是定义或使用其他对象的基础。包括一般对象、函数对象和错误对象。
如:Object、Function、Boolean、Symbol、Error 等。
- 数字和日期对象:用来表示数字、日期和执行数学计算的对象。
如:Number、Math、Date。
- 字符串:用来表示和操作字符串的对象。
如:String、RegExp
- 可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。
如:Array。
- 使用键的集合对象:这些集合对象在存储时会使用到键,支持按照插入顺序来迭代元素。
如:Map、Set、WeakMap、WeakSet。
- 矢量集合:SIMD 矢量集合中的数据会被组织为一个数据序列。
如:SIMD 等。
- 结构化数据:这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。
如:JSON 数据。
- 控制抽象对象。如:Promise、Generator 等。
- 反射。如:Reflect、Proxy。
- 国际化:为了支持多语言处理而加入的 ECMAScript 的对象。
如:Intl、Intl.Collator 等。
- WebAssembly
- 其他。如 arguments
总结
- JS 中的内置对象主要指的是在程序执行前存在全局作用域中的由 JS 定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。
- 一般经常用到的如 全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构造函数 如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象等。
JS 脚本延迟加载的方式
- defer:异步加载,延迟执行(html 加载完再执行)
- async:异步加载,加载完立即执行(会阻塞 html 页面解析)
- 动态创建 DOM 方式:对文档加载事件进行监听,当文档加载完后,再动态创建 script 标签来引入 JS 脚本。
- setTimeout 延迟方法:设置一个定时器来延迟加载 JS 脚本文件
- 让 JS 最后加载:将 JS 脚本文件放在文档底部,来使 JS 脚本尽可能最后加载执行。
JS 类数组对象
JS 类数组对象:一个拥有 length 属性和若干索引属性的对象。
和数组类似,但不能调用数组的方法。
常见的类数组对象有:
- argument 和 DOM 方法的返回结果
- 函数(因为含有 length 属性值,代表可接收的参数个数)
类数组转换为数组的方法:
- 通过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arrayLike); |
- 通过 call 调用数组的 splice 方法来实现转换
Array.prototype.splice.call(arrayLike, 0); |
- 通过 apply 调用数组的 concat 方法来实现转换
Array.prototype.concat.apply([], arrayLike); |
- 通过 Array.from() 实现转换
Array.from(arrayLike); |
数组有哪些原生方法
- 数组和字符串的转换方法:toString()、toLocalString()、join(),其中 join()可以指定转换为字符串时的分隔符
- 数组尾部操作的方法:pop()和 push(),push() 方法可以传入多个参数
- 数组首部操作的方法:shift() 删除和 unshift() 添加
- 重排序的方法:reverse() 和 sort(),sort() 可传入一个函数进行比较,传入前后两个值,如果返回值为正数,则交换两个参数的位置
- 数组连接:concat() 返回拼接好的数组,不影响原数组。
- 数组截取:splice()
- 影响原数组特定项的索引的方法,indexOf()和 lastIndexOf()
- 迭代方法:every()、some()、filter()、map()、forEach()方法
- 数组归并方法:reduce()、reduceRight()方法
Promise.race() 应用场景
race() 和 all() 一样,接受的参数是一个每项都是 Promise 的数组,但是与 all 不同的是,当最先执行完的事件执行完后,就直接返回该 promise 对象的值。
如果第一个 promise 对象状态变成 resolved,则自身的状态就变成了 resolved;反之,第一个 promise 变成 rejected,则自身状态就会变成 rejected。
Promise.race() 应用场景:
- http 请求时,做超时判断时,设置一个定时器,当定时器时间到了时间就在页面提醒请求超时。
// 请求某个图片资源 |
Promise.all() 应用场景
它可以将多个 promise 实例包装成一个新的 Promise 实例。同时,成功和失败的返回值是不同的,成功时返回一个结构数组,而失败时则返回最先被 reject 失败状态的值。
Promise.all() 中传入的是数组,返回的也是数组,并会进行映射,传入的 Promise 对象返回值是按照顺序在数组中排列的,但是注意他们执行的顺序并不是按照顺序的,除非可迭代对象为空。
注意:Promise.all 获得的成功结果的数组中的数据顺序和 Promise.all()接收的数组顺序是一致的,这样当遇到多个请求并根据请求顺序获取和使用数据的场景,可以使用 Promise.all 来解决。
使用 requestAnimationFrame 实现一个循环定时器
function setInterval(callback, interval) { |
⾸先 requestAnimationFrame ⾃带函数节流功能,基本可以保证在 16.6 毫秒内只执⾏⼀次(不掉
帧的情况下),并且该函数的延时效果是精确的,没有其他定时器时间不准的问题。