面试题-JavaScript
面试题-JavaScript
JS 中的数据类型及区别
基本类型(值类型)
在内存中占据固定大小,保存在栈内存中
- Number(数字)
- String(字符串)
- Boolean(布尔)
- Symbol(符号)
- null(空)
- undefined(未定义)
引用类型(复杂数据类型)
保存在堆内存中,栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址
- Object(对象)
- Array(数组)
- Date(日期)
- RegExp(正则表达式)
- Function(函数)
- …
使用场景
Symbol:使用 Symbol 来作为对象属性名(key) 利用该特性,把一些不需要对外操作和访问的属性使用 Symbol 来定义
BigInt:由于在 Number 与 BigInt 之间进行转换会损失精度,因而建议仅在值可能大于 253 时使用 BigInt 类型,并且不在两种类型之间进行相互转换
JS 中的数据类型检测方案
typeof
1 | console.log(typeof 1); // number |
优点:能够快速区分基本数据类型
缺点:不能将 Object、Array 和 Null 区分,都返回 object
注意:
1 | console.log(typeof null == object); // true |
instanceof
1 | console.log(1 instanceof Number); // false |
优点:能够区分 Array、Object 和 Function,适合用于判断自定义的类实例对象
缺点:Number,Boolean,String 基本数据类型不能判断
Object.prototype.toString.call()
1 | var toString = Object.prototype.toString; |
优点:精准判断数据类型
缺点:写法繁琐不容易记,推荐进行封装后使用
instanceof 的作用
用于判断一个引用类型是否属于某构造函数;
还可以在继承关系中用来判断一个实例是否属于它的父类型。
instanceof 和 typeof 的区别:
typeof 在对值类型 number、string、boolean 、null 、 undefined、 以及引用类型的 function 的反应是精准的;但是,对于对象{ } 、数组[ ] 、null 都会返回 object
为了弥补这一点,instanceof 从原型的角度,来判断某引用属于哪个构造函数,从而判定它的数据类型。
var & let & const
ES6 之前创建变量用的是 var,之后创建变量用的是 let / const
三者区别:
- var 定义的变量,
没有块的概念,可以跨块访问
,不能跨函数访问
let 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问
const 用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,且不能修改 - var 可以
先使用,后声明
,因为存在变量提升;let 必须先声明后使用 - var 是允许在相同作用域内
重复声明同一个变量
的,而 let 与 const 不允许这一现象 - 在全局上下文中,基于 let 声明的全局变量和全局对象 GO(window)没有任何关系;
var 声明的变量会和 GO 有映射关系
字符串常用方法
数组常用方法
常用正则表达式
1 | //(1)匹配 16 进制颜色值 |
arguments
arguments
对象是函数中传递的参数值的集合
arguments
是一个传递给函数的参数的类数组对象,因为它有一个 length 属性,我们可以使用数组索引表示法 arguments[1]
来访问单个值,但它没有数组中的内置方法,如:forEach、reduce、filter、map
注意:箭头函数中没有 arguments
对象
解构赋值
解构赋值是 ES6 引入的新特性,数组中的值或对象的属性取出,赋值给其他变量
1 | let a, b, rest; |
1 | let employee = { |
扩展运算符...
如果是字符串、数组就是在语法层面展开;如果是对象,就进行属性拷贝
1 | let str = "hello"; |
深浅拷贝
浅拷贝
创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝
如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象
实现方法
Object.assign()
方法: 用于将所有可枚举属性的值从一个或多个源对象复制到目标对象,返回修改后的对象Array.prototype.slice()
:返回一个新的数组对象,由 begin 和 end(不包括 end)决定的原数组的浅拷贝- 扩展运算符
...
深拷贝
将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象
利用 JSON 的方法
1
2
3JSON.parse(JSON.stringify(object));
// 会忽略undefined、symbol、函数,不能处理正则、new Date(),不能解决循环引用封装方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function cloneDeep(target,map = new WeakMap()) {
if(typeOf taret === 'object'){
let cloneTarget = Array.isArray(target) ? [] : {};
if(map.get(target)) {
return target;
}
map.set(target, cloneTarget);
for(const key in target){
cloneTarget[key] = cloneDeep(target[key], map);
}
return cloneTarget
}else{
return target
}
}
&&、||、!!
- &&: 叫逻辑与,在其操作数中找到第一个假值表达式并返回它,如果没有找到任何假值表达式,则返回最后一个真值表达式
- ||: 叫逻辑或,在其操作数中找到第一个真值表达式并返回它
- !!: 运算符可以将右侧的值强制转换为布尔值,这也是将值转换为布尔值的一种简单方法
作用域和作用域链
作用域
作用域定义了变量和函数的可见性或可访问性
通俗来说,就是一个变量或函数能不能被访问或引用,是由它的作用域决定的
三种作用域
- 全局作用域
- 函数作用域(局部作用域)
- 块作用域
作用
- 作用域最大的用处就是
隔离变量
- 不同作用域下同名变量不会有冲突
作用域链
一般情况下,变量到创建该变量的函数的作用域中取值,但是如果在当前作用域中没有查到,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链
闭包
通俗来说,闭包就是能够读取其他函数内部变量的函数
闭包形成的条件:
函数的嵌套
内部函数引用外部函数的局部变量
闭包的用途:
可以读取函数内部的变量
让这些变量的值始终保持在内存中(防止被回收)
闭包应用场景
发送 ajax 请求成功|失败的回调
setTimeout 的延时回调
或者一个函数内部返回另一个匿名函数
JS 中 this 的情况
- 普通函数调用:通过 函数名() 直接调用,
this
指向全局对象window
(注意 let 定义的变量不是 window 属性,只有 window.xxx 定义的才是) - 构造函数调用:函数作为构造函数,用 new 关键字调用时:
this
指向新new出的对象
- 对象函数调用:通过对象.函数名() 调用的:
this
指向这个对象
- 箭头函数调用:箭头函数里面没有 this,所以
永远是上层作用域this
(上下文) apply
和call
调用:函数体内 this 的指向的是 call / apply 方法第一个参数
,若为空默认是指向全局对象 window- 函数作为 window 内置函数的回调函数调用:this 指向 window(如 setInterval setTimeout 等)
call & apply & bind
call、apply、bind 都是改变 this 指向的方法
call
1 | fn.call(null, 1, 2); |
第一个参数为改变的 this 指向谁,后面的参数都是 fn 函数的参数;会立即执行
apply
1 | fn.apply(null, [1, 2]); |
第一个参数为改变的 this 指向谁,第二个参数是一个数组,是 fn 函数的参数;会立即执行
bind
1 | fn.bind(null, 1, 2); |
与 call()
类似;不会立即执行
箭头函数的特性
1 | (number) => { |
箭头函数没有自己的this
;会捕获其所在的上下文的 this 值,作为自己的 this 值箭头函数没有constructor
;是匿名函数,不能作为构造函数,不能通过 new 调用;没有new.target 属性
;在通过 new 运算符被初始化的函数或构造方法中,new.target 返回一个指向构造方法或函数的引用箭头函数不绑定 Arguments 对象
- 箭头函数没有原型属性 Fn.prototype 值为 undefined
模板字符串
允许嵌入表达式的字符串,使用反引号 (` `) 来代替普通字符串中的用双引号和单引号
1 | // ES5 |
Set
Set 是唯一值的集合
每个值在 Set 中只能出现一次
一个 Set 可以容纳任何数据类型的任何值
创建实例
1 | let set1 = new Set(); |
操作
方法或属性 | 说明 |
---|---|
size | 返回元素的个数 |
add() | 向 Set 添加新元素 |
clear() | 从 Set 中删除所有元素 |
delete() | 删除由其值指定的元素 |
has() | 如果值存在则返回 true,反之返回 false |
forEach() | 为每个元素调用回调 |
keys() | 返回 Set 对象中值的数组 |
values() | 与 keys() 相同 |
Map
Map 以键值对的形式进行数据保存,并且能够记住键的原始插入顺序
Map 中的一个键只能出现一次;它在 Map 的集合中是独一无二的
创建实例
1 | let map = new Map(); |
操作
方法或属性 | 说明 |
---|---|
size | 返回元素的个数 |
set() | 为 Map 对象中的键设置值 |
get() | 获取 Map 对象中键的值 |
keys() | 返回 Map 对象中键的数组 |
values() | 与 keys() 相同 |
原型 & 原型链
- 每个 class 都有显示原型
prototype
- 每个实例都有隐式原型
__proto__
- 实例的
__proto__
指向对应 class 的prototype
原型
在 JS 中,每当定义一个对象(函数也是对象)时,对象中都会包含一些预定义的属性。其中每个函数对象都有一个prototype
属性,这个属性指向函数的原型对象
原型链
函数的原型对象 constructor 默认指向函数本身,原型对象除了有原型属性外,为了实现继承,还有一个原型指针__proto__
,该指针是指向上一层的原型对象,而上一层的原型对象的结构依然类似
因此可以利用__proto__
一直指向 Object 的原型对象上,而 Object 原型对象用 Object.prototype.__proto__ = null
表示原型链顶端
js 获取原型的方法
obj.proto
obj.constructor.prototype
Object.getPrototypeOf(obj)
new 运算符的实现机制
- 首先创建了一个新的
空对象
设置原型
,将对象的原型设置为函数的prototype
对象- 让函数的
this
指向这个对象,执行构造函数的代码(为这个新对象添加属性) - 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象
DOM & BOM
- DOM 指的是文档对象模型,它指的是把文档当做一个对象来对待,这个对象主要定义了处理网页内容的方法和接口。
- BOM 指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的法和接口;document 对象也是 BOM 的 window 对象的子对象。
DOM 操作
创建新节点
1 | createDocumentFragment(); //创建一个 DOM 片段 |
添加、移除、替换、插入
1 | appendChild(node); // 添加 |
查找
1 | getElementById(id); // 通过 id |
属性操作
1 | getAttribute(key); // 获取属性 |
事件传播
事件传播有三个阶段
- 捕获阶段 – 事件从 window 开始,然后向下到每个元素,直到到达目标元素事件或
event.target
- 目标阶段 – 事件已达到目标元素
- 冒泡阶段 – 事件从目标元素冒泡,然后上升到每个元素,直到到达 window
addEventListener()
有第三个参数 useCapture,其默认值为 false
,事件将在冒泡阶段中发生,如果为 true
,则事件将在捕获阶段中发生
addEventListener
详细内容可以看看 MDN 文档 MDN-addEventListener
事件冒泡
冒泡指的是事件从目标元素冒泡到 document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行
事件捕获
捕获指的是事件从 document 一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有则执行
阻止事件默认行为
1 | e.preventDefault(); |
事件委托(事件代理)
事件委托指的是把原本需要绑定在子元素的响应事件委托给父元素,让父元素担当事件监听的职务
1 | <ul id="list"> |
1 | // 获取列表元素 |
EventLoop 事件循环
JavaScript 是单线程的,为了防止一个函数执行时间过长阻塞后面的代码,所以会先将同步代码压入执行栈中,依次执行,将异步代码推入异步队列,异步队列又分为宏任务队列和微任务队列,因为宏任务队列的执行时间较长,所以微任务队列要优先于宏任务队列
执行顺序:同步任务 ==> 微任务 ==> 宏任务
微任务队列的代表:Promise.then、MutationObserver
宏任务的话就是:setImmediate、setTimeout、setInterval
浏览器中的事件环(Event Loop)
在主线程执行过程中同步任务会立即执行,遇到异步任务时不会立马执行,会将异步任务放入一个”任务队列”中
当”执行栈”中的所有任务执行完毕,就会去”任务队列”中将对应的任务事件放入执行栈中执行,主线程就是这样重复执行上面的步骤形成一个循环
步骤
- 函数入栈,当 Stack 中执行到异步任务的时候,就将他丢给 WebAPIs,接着执行同步任务,直到 Stack 为空;
- 此期间 WebAPIs 完成这个事件,把回调函数放入队列中等待执行(微任务放到微任务队列,宏任务放到宏任务队列)
- 执行栈为空时,Event Loop 把微任务队列执行清空;
- 微任务队列清空后,进入宏任务队列,取队列的第一项任务放入 Stack(栈) 中执行,执行完成后,查看微任务队列是否有任务,有的话,清空微任务队列
- 重复,继续从宏任务中取任务执行,执行完成之后,继续清空微任务,如此反复循环,直至清空所有的任务
浏览器中的任务源(task):
宏任务(macrotask)
:
宿主环境提供的,比如浏览器
ajax、setTimeout、setInterval、setTmmediate(只兼容 ie)、script、requestAnimationFrame、messageChannel、UI 渲染、一些浏览器 api微任务(microtask)
:
语言本身提供的,比如 promise.then
then、queueMicrotask(基于 then)、mutationObserver(浏览器提供)、messageChanne、MutationObserve
Ajax
一种异步通信的方法,通过直接由 js 脚本向服务器发起 http 通信,然后根据服务器返回的数据,更新网页的相应部分,而不用刷新整个页面的一种方法
1 | //1:创建Ajax对象 |
setTimeout、Promise、Async / Await 的区别
setTimeout
setTimeout 的回调函数放到宏任务队列里,等到执行栈清空以后执行
Promise
Promise 本身是同步的立即执行函数, 当在 executor 中执行 resolve 或者 reject 的时候, 此时是异步操作,会先执行 then / catch 等,当主栈完成后,才会去调用 resolve / reject 中存放的方法执行
1 | console.log("script start"); |
async / await
async / await 是一个语法糖
async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句
1 | async function async1() { |
Async / Await 如何通过同步的方式实现异步
Async/Await 就是一个自执行的 generate 函数
利用 generate 函数的特性把异步的代码写成“同步”的形式,第一个请求的返回值作为后面一个请求的参数,其中每一个参数都是一个 promise 对象
节流 & 防抖
- 节流:事件触发后,规定时间内,事件处理函数不能再次被调用
- 防抖:在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时
使用场景
- 节流:滚动加载更多、搜索框搜的索联想功能、高频点击、表单重复提交……
- 防抖:搜索框搜索输入,并在输入完以后自动搜索、手机号,邮箱验证输入检测、窗口大小 resize 变化后,再重新渲染
代码
1 | /** |
数组扁平化转换
在说到模版编译的时候,有可能会提到数组的转换
1 | [1,2,3,[4,5]] |
1 | // 测试数组 |
参考
-
感谢你赐予我前进的力量