1. 作用域和作用域链
作用域:变量和函数的有效访问范围。分为全局作用域
、函数作用域
和块作用域
。
作用域链:当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链
。
其他相关概念:
💡 变量对象
JavaScript执行一段可执行代码时,会创建对应的执行上下文,变量对象是与 执行上下文 相关的数据作用域,存储了在上下文中定义的 变量
和 函数
。
💡变量提升
使用var
声明的变量,无论其实际声明的位置在何处,都会被视为声明于所在函数的顶部(如果声明不在函数内,则视为在全局作用域的顶部),这就是变量提升
。总结来看:
JS
会将变量的声明提升到JS
顶部执行,因此对于这种语句:var a = 2;
其实上js会将其分为var a;
和a = 2;
两部分,并且将var a;
这一步提升到顶部执行。- 变量提升的本质其实是由于
JS
引擎在编译的时候,就将所有的变量声明了,因此在执行的时候,所有的变量都已经完成声明。 - 当有多个同名变量声明的时候,函数声明会覆盖其他的声明。如果有多个函数声明,则是由最后的一个函数声明覆盖之前所有的声明。
let
和const
不存在变量提升(有些文章说let
和const
也会提升,这个值的商榷)
💡暂时性死区(TDZ)
当 JS
引擎编译代码块并发现变量声明时,它会在面对 var
的情况下将声明提升到函数或全局作用域的顶部,而面对 let
或 const
时会将声明放在暂时性死区内。通常使用暂时性死区
用来描述let
和const
在达到声明处之前都是无法访问的这一特性。
2. Promise
基本概念
Promise
是 ES6
新增的语法,是一种异步编程的解决方案,Promise
本质上是一个绑定了回调的对象。 Promise
在一定程度上解决了回调函数的书写结构问题,解决了回调地狱的问题。Promise
可以看作是一个状态机,它有三种状态:pending
、fulfilled
和rejected
,初始状态是 pending
,可以通过函数 resolve()
把状态变为 fulfilled
,或者通过函数 reject()
把状态变为 rejected
,状态一经改变就不能再次变化。
常用API
Promise.all(): 接受多个Promise实例组成的数组作为唯一参数,返回一个Promise实例。只有受监视的所有Promise实例都完成,返回的Promise实例才会完成,只要有一个被拒绝,返回的Promise实例就会被立即拒绝。
Promise.race(): 接受多个Promise实例组成的数组作为唯一参数,返回一个Promise实例,只要有一个被解决(完成或拒绝),返回的Promise实例就会被立即解决(完成或拒绝)。
Promise.any(): 接受多个Promise实例组成的数组作为唯一参数,返回一个Promise实例,只要有一个被完成,返回的Promise实例就会变成完成状态,如果所有的promise实例都被拒绝,返回的promise实例就会变成拒绝状态。
Promise.allSettled(): 接受多个Promise实例组成的数组作为唯一参数,返回一个Promise实例。当所有Promise实例都解决时,返回的Promise会被完成,完成的结果是一个包含所有promise结果的对象数组。
实现原理
promise取消实现
- 借助Promise.reject(),在封装promise的时候,对外暴露一下promise中的reject方法
- 借助Promise.race()的特性,将一个辅助promise的reject暴露出来,调用辅助promise的reject即可让Promise.race()提前结束执行
3. async/await
async/await
是 Generator 函数的语法糖,作用是用同步的方式,执行异步操作,最常见的用法就是用同步的写法来替代promise.then()
的嵌套问题。
// 后续异步请求都依赖于前一个请求的结果
function fn() {
request().then(res1 => {
request(res1).then(res2 => {
request(res2).then(res3 => {
// ...
})
})
})
}
// 替代写法
async function fn() {
const res1 = await request()
const res2 = await request(res1);
const res3 = await request(res2);
// ...
}
async
函数的实现原理,就是将 Generator
函数和自动执行器,包装在一个函数里。
实现
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
// spawn是自动执行器函数,自动调用generator的next方法
return spawn(function* () {
// ...
});
}
// spawn函数实现
function spawn(genF) {
// async函数最终返回的是一个promise
return new Promise(function(resolve, reject) {
const iterator = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
// next.done为true时,说明走到了iterator的最后一步,直接返回结果
if(next.done) {
return resolve(next.value);
}
// next.value有可能是常量或promise,所以这里采用Promise.resolve处理
Promise.resolve(next.value).then(function(v) {
// 递归调用step
step(function() {
return iterator.next(v);
});
}, function(e) {
step(function() {
return iterator.throw(e);
});
});
}
step(function() { return iterator.next(); });
});
}
4. ES6常用新特性
5. 数据类型的判断方法
6. 继承
7. 原型链
8. 闭包
闭包
是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0](); // 3
data[1](); // 3
data[2](); // 3
// 改善输出结果
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function() {
console.log(i);
}
})(i);
}
data[0](); // 0
data[1](); // 1
data[2](); // 2
9. 事件循环机制
- JS引擎逐行扫描JS代码,遇到
同步任务
会将其添加到JS引擎线程的执行栈
中。 - 遇到异步任务,交由浏览器渲染进程中的相应线程(比如
setTimeout
交给定时器线程
,http
请求交给异步网络请求线程
)管理,当异步任务有了运行结果,事件触发线程就会将异步任务的事件回调依次放进任务队列
的末尾。该线程维护一个微任务队列
和一个宏任务队列
。 执行栈
中的任务执行完之后,JS引擎会从宏任务队列队首取一个宏任务到执行栈中开始执行;- 宏任务执行完毕后,JS引擎会依次从当前微任务队列中取出微任务到执行栈中执行;
- 当前微任务队列为空,回到3,如此循环执行
10. 浅拷贝和深拷贝
浅拷贝
拷贝时,只是拷贝了基本类型的值,而引用类型的值,复制的是引用类型的值的地址,该值指向同一个内存地址,会互相影响。
实现方式一: Object.assign(target, source1, source2)
const obj = {
a: '123',
b: 234,
c: true,
d: null
}
const newObj = Object.assign({}, obj);
实现方式二: ES6扩展运算符
const obj = {
a: '123',
b: 234,
c: true,
d: null
}
const newObj = {...obj};
实现方式三: 递归
function shallowClone(obj) {
// 判断拷贝的obj是对象还是数组
const objClone = Array.isArray(obj) ? [] : {};
for (key in obj) {
if (obj.hasOwnProperty(key)) {
objClone[key] = obj[key]; // 直接拷贝
}
}
return objClone;
}
实现方式四:数组
- Array.concat()
- Array.slice()
深拷贝
拷贝后的对象与原来的对象是完全隔离,即使是引用类型的属性,也不会相互影响。
实现方式一: JSON.stringify
序列化和JSON.parse
反序列化
缺点:对象中undefined
、function
、symbol
这三种类型的值会被过滤掉
const obj = {
a: '123',
b: 234,
c: true,
d: null,
e: function() {console.log('test')},
h: new Set([4,3,null]),
i: Symbol('fsd'),
k: new Map([ ["name", "test"], ["title", "Author"] ])
}
console.log(JSON.stringify(obj)); // {"a":"123","b":234,"c":true,"d":null,"h":{},"k":{}}
const newObj = JSON.parse(JSON.stringify(obj));
实现方式二: 递归
function deepClone(obj) {
// 判断拷贝的obj是对象还是数组
const objClone = Array.isArray(obj) ? [] : {};
if (obj && typeof obj === "object") { // obj不能为空,并且是对象或者是数组,因为null也是object
for (key in obj) {
if (obj.hasOwnProperty(key)) {
if (obj[key] && typeof obj[key] === "object") { // obj里面属性值不为空并且还是对象,进行深度拷贝
objClone[key] = deepClone(obj[key]); // 递归进行深度的拷贝
} else {
objClone[key] = obj[key]; // 直接拷贝
}
}
}
}
return objClone;
}
11. this指向
12. 箭头函数与普通函数的区别
- 没有
this
、super
、arguments
及new.target
绑定,this
、super
、arguments
及new.target
的值由所在的、最靠近的非箭头函数来决定; - 不能被使用
new
调用; - 没有原型;
- 不能更改
this
; - 不允许重复的具名参数,传统的函数中参数可以重名。
13. 防抖与节流
防抖
指定时间内函数多次调用都会被重置,只会在最后一次触发结束后延时执行
常用使用场景:
- 按钮多次点击(发送验证码按钮、提交按钮、支付按钮等)
- input输入框输入事件
/**
* @fn : 要执行的函数
* @delay : 执行函数的时间间隔(毫秒)
*/
function debounce(fn, delay) {
let timer;
return function(...args) {
timer && clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay)
}
}
节流
指定时间内,函数多次调用只执行一次
常用使用场景:
- 浏览器窗口
resize
、scroll
事件 - 鼠标移动
mousemove
事件 - 上传进度事件
- 文档编辑隔一段时间自动保存
/**
* @fn : 要执行的函数
* @delay : 每次函数的时间间隔
*/
function throttle(fn, delay) {
let timer; // 定时器
return function(...args) {
if(timer) return;
timer = setTimeout(() => {
timer = null;
fn.apply(this, args);
}, delay);
}
}
14. instanceof原理
function instance_of(left, right) {
const RP = right.prototype; // 构造函数的原型
while(true) {
if (left === null) return false;
if (left === RP) return true;
left = Object.getPrototypeOf(left); // 沿着原型链重新赋值
}
}
15. ==
和 ===
==
和!=
— 先转换数据类型再比较===
和!==
— 仅比较而不转换数据类型
==
和 !=
遵循下列转换规则:
- 如果有一个操作数是布尔值,则在比较相等性之前先将其转换为数值:false转换为0,true转换为1
- 一个是字符串,一个是数值,则在比较相等性之前先将字符串转换为数值
- 一个是对象,一个不是对象,则调用对象的
valueOf()
方法,得到基本数据类型后再按前面的规则比较 null
和undefined
不能转换为其他任何值
==
和!=
在比较时需要遵循下列规则:
null
和undefined
是相等的NaN
与任何值都不相等,包括它自己- 两个操作数都是对象,如果两个操作数指向同一个对象,则为
true
,否则为false
特殊情况比较结果:
表达式 | 值 |
---|---|
null == undefined | true |
NaN == NaN | false |
false == 0 | true |
true == 1 | true |
true == 2 | false |
undefined == 0 | false |
null == 0 | false |
16. call、bind、apply原理
MDN:
call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
/**
* call的模拟实现
*/
Function.prototype.myCall = function(context) {
// this指向调用myCall方法的函数
if (typeof this !== 'function') {
throw new TypeError('Error, caller must be a function')
}
context = context || window;
// 将调用myCall的函数赋值为context对象的方法
context.fn = this;
const args = [...arguments].slice(1);
// 利用context.fn()来执行原函数,原函数中的this就指向了context
const result = context.fn(...args) ;
delete context.fn ;
return result;
}
MDN:
apply() 方法调用一个具有给定 this 值的函数,以及以一个数组(或一个类数组对象)的形式提供的参数。
/**
* apply的模拟实现
* 原理与call一样,区别仅在于参数的处理上
*/
Function.prototype.myApply = function(context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
context = context || window;
context.fn = this;
let result // 处理参数和 call 有区别
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn;
return result;
}
MDN:
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
/**
* bind的模拟实现
*
*/
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
const _this = this
// 保存调用bind时传递的参数
const args = [...arguments].slice(1)
// 返回一个函数
return function F() {
// 因为返回了一个函数,我们可以 new F(),所以需要判断
if (this instanceof F) {
return new _this(...args, ...arguments)
}
// 修改this及合并参数
return _this.apply(context, args.concat(...arguments))
}
}
17. new的原理
使用 new
来调用函数,会自动执行下面的操作:
- 创建一个空对象
{}
- 将这个新对象的原型对象指向构造函数的原型对象,以继承原型上的方法
- 将构造函数中的this指向到新创建的对象并执行构造函数,以获取实例属性
- 如果构造函数执行后返回了对象,就将该对象作为结果返回;否则就将上面创建的新对象作为结果返回
// 简单实现
function myNew(Fn, ...args) {
// 原型式继承
const obj = Object.create(Fn.prototype);
// 执行构造函数
const res = Fn.apply(obj, args);
return res instanceof Object ? res : obj;
}
function Person(name, age) {
this.name = name;
this.age = age;
return this;
}
const student1 = myNew(Person, 'Mike', 20);
console.log(student1); // {"name":"Mike","age":20}
18. 事件委托
事件传播的 3 个阶段:
- 捕获阶段(Capturing phase)—— 从window,document 和根元素开始,事件向下扩散至目标元素的祖先
- 目标阶段(Target phase)—— 事件到达目标元素
- 冒泡阶段(Bubbling phase)—— 事件从元素上开始冒泡,一直到根元素,document 和 window。
为何使用事件委托?
在 JavaScript 中,页面内事件处理程序的个数会直接影响页面的整体性能,因为每个事件处理程序都是对象,对象会占用内存,内存中的对象越多,页面的性能则越差。此外,事件处理程序需要与 DOM 节点进行交互,访问 DOM 的次数越多,引起浏览器重绘和重排的次数也就越多,从而影响页面的性能。
事件委托实现原理
事件委托是利用事件的冒泡原理
来实现的,大致可以分为三个步骤:
- 确定要添加事件元素的父级元素;
- 给父元素定义事件,监听子元素的冒泡事件;
- 使用 event.target 来定位触发事件冒泡的子元素。
19. Generator(生成器)和 Iterator(迭代器)
20. 纯函数
定义
一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数。
// 纯函数
function sum1(a, b) {
return a + b
}
// 非纯函数,因为函数执行过程中改变了message的值,被认定产生了副作用
let message = 'hello'
function sum2(a, b) {
message = 'hi'
return a + b
}
const nums = [1, 2, 3, 4, 5]
// slice是纯函数
const newNums1 = nums.slice(1, 3)
// splice不是纯函数,因为它改改变了原数组的值
const newNums2 = nums.splice(1, 3);
纯函数的价值
纯函数非常靠谱,执行一个纯函数你不用担心它会干什么坏事,它不会产生不可预料的行为,也不会对外部产生影响。虽然我们的程序不可避免的会产生副作用,比如 HTTP 调用、IO 操作等,但尽可能多的地方使用纯函数将大大提高程序的可读性、可调试性和可测试性,代码重构时也不必担心没注意到的副作用而搞乱了整个应用。
纯函数在函数式编程中被大量使用,而且诸如 ReactJS 和 Redux 等优质的库都要求使用纯函数。
正确地使用纯函数可以产生更加高质量的代码,并且也是一种更加干净的编码方式。
21. Ajax、Axios和fetch的区别
Ajax
是指一种无需刷新页面即可向服务器请求数据的技术。核心是XMLHttpRequest
对象。Axios
是通过Promise
实现XHR
封装的JavaScript
库,在浏览器和Node.js
环境中都可以运行。fetch
是JavaScript
提供的原生API,是Ajax
的替代方案,没有使用XMLHttpRequest
对象。只可以在浏览器上使用。
22. 常用的设计模式
单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点。实现的方法为先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。
应用场景:弹窗组件同一时间只能展示一个
观察者模式
一个对象(观察者)订阅另一个对象(主题),当主题被激活的时候,触发观察者里面的事件。
应用场景:Vue响应式原理中的Dep
和Watcher
发布-订阅模式
发布订阅模式与观察者模式相比,发布订阅模式中有三个角色,发布者 Publisher
,事件调度中心 Event Channel
,订阅者 Subscriber
。
订阅者把自己想要订阅的事件注册到调度中心,当发布者发布事件到调度中心,再由调度中心统一调度订阅者注册到调度中心的处理代码。
应用场景:Vue中的$on,$emit
用来组件间通信
23. 发布订阅模式和观察者模式
- 角色上看
- 观察者模式里只有两个角色:观察者和被观察者。
- 发布订阅模式里有三种角色:发布者、订阅者、调度器(第三者)。
- 耦合关系上看
- 观察者和被观察者是松耦合的关系,两个主体存在直接交互
- 发布者和订阅者则完全不存在耦合,两个主体是借助第三方主体来进行间接交互
24. JS的模块化规范
25. 传值与传址
- 传值:在赋值过程中,首先对值进行了一份拷贝,而后将这份拷贝存储到一个变量、对象属性或数组元素中。拷贝的值和原始的值是完全独立、互不影响的。当一份数据通过值传递给一个函数,实际上被传递的不是数据本身,而是数据的一份拷贝。因此,如果函数修改了这个值,影响到的只是数据的那份拷贝,而并不影响数据本身。
- 传址:在赋值过程中,变量实际上存储的是数据的地址(对数据的引用),而不是原始数据或者是数据的拷贝。如果值通过一个地址发生了改变,这个改变也会通过原始地址表现出来。