js 知识点整理
# 数据类型
Boolean、Null、Undefined、Number、BigInt、String、Symbol 和 Object
# 判断数据类型
- typeof:对于
null
及数组、对象,typeof均检测出为object - instanceof:测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性
- constructor:undefined和null没有contructor属性;可被修改,所以不准确
- Object.prototype.toString.call
# var let const
提死复始作
声明方式 | 变量提升 | 暂时性死区 | 重复声明 | 初始值 | 作用域 |
---|---|---|---|---|---|
var | 允许 | 不存在 | 允许 | 不需要 | 函数 |
let | 不允许 | 存在 | 不允许 | 不需要 | 块 |
const | 不允许 | 存在 | 不允许 | 需要 | 块 |
变量的赋值可以分为三个阶段:
- 创建变量,在内存中开辟空间
- 初始化变量,将变量初始化为undefined
- 真正赋值
关于let
、var
和 function
:
let
的「创建」过程被提升了,但是初始化没有提升var
的「创建」和「初始化」都被提升了function
的「创建」「初始化」和「赋值」都被提升了
# 类型转换
JS 中在使用运算符号或者对比符时,会自带隐式转换,规则如下:
- -、*、/、% :一律转换成数值后计算
- +:
- 数字 + 字符串 = 字符串, 运算顺序是从左到右
- 数字 + 对象, 优先调用对象的
valueOf
->toString
- 数字 +
boolean/null
-> 数字 - 数字 +
undefined
->NaN
[1].toString() === '1'
{}.toString() === '[object object]'
NaN
!==NaN
、+undefined 为 NaN
# new
# 执行过程
- 新生成一个对象
- 链接到原型:
obj.__proto__ = Constructor.prototype
- 绑定this:
apply
- 返回新对象(如果构造函数有自己 retrun 时,则返回该值)
# 代码实现
const myNew = () => {
let obj = {};
let Constructor = [].shift.call(arguments);
obj.__proto__ = Constructor.prototype;
let result = Constructor.apply(obj, arguments);
return result instanceof Object ? result : obj;
}
2
3
4
5
6
7
# this
由于 JS 的设计原理: 在函数中,可以引用运行环境中的变量。因此就需要一个机制来让我们可以在函数体内部获取当前的运行环境,这便是this
。
- 函数调用:this指向的是window
- 方法调用:this指向调用当前方法的对象
- 构造函数调用:如果函数是new调用的,此时this被绑定到创建出来的新对象上
- 上下文调用:call、apply
- 箭头函数:根据外部作用域来决定this
# 作用域
- 作用域其实可理解为该上下文中声明的 变量和声明的作用范围。可分为 块级作用域 和 函数作用域。js的作用域: 静态作用域(词法作用域),只管在哪声明,不管在哪调用
- 作用域链可以理解为一组对象列表,包含 父级和自身的变量对象。因此我们可以在执行上下文中访问到父级甚至全局的变量
# 闭包
形象描述:当一个函数被创建并传递或从另一个函数返回时,它会携带一个背包。背包中是函数声明时作用域内的所有变量。
产生条件:
- 函数调用结束后函数的执行上下文销毁,内部变量会被释放。
- 词法作用域:一个函数可以访问在它的调用上下文中定义的变量。
定义:父函数被销毁的情况下,返回出的子函数的
[[scope]]
中仍然保留着父级的单变量对象和作用域链,因此可以继续访问到父级的变量对象,这样的函数称为闭包。闭包会产生一个很经典的问题:
- 多个子函数的
[[scope]]
都是同时指向父级,是完全共享的。因此当父级的变量对象被修改时,所有子函数都受到影响。
- 多个子函数的
解决:
- 变量可以通过 函数参数的形式 传入,避免使用默认的
[[scope]]
向上查找 - 使用
setTimeout
包裹,通过第三个参数传入 - 使用 块级作用域,让变量成为自己上下文的属性,避免共享
- 变量可以通过 函数参数的形式 传入,避免使用默认的
作用:
- 私有化变量
- 模拟块级作用域
- 创建模块
节流防抖 就应用了闭包。
# 原型链
# 组员
原型 prototype:一个简单的对象,用于实现对象的 属性继承。可以简单的理解成对象的爹,通过
obj.__proto__
进行访问。构造函数 constructor: 可以通过
new
来 新建一个对象 的函数。实例 instance:通过构造函数和
new
创建出来的对象,便是实例。 实例通过__proto__
指向原型,通过constructor
指向构造函数。
# 组员关系
instance.__proto__ === prototype
prototype.constructor === constructor
constructor.prototype === prototype
2
3
4
5
# 原型链
一个对象会有一个原型(对象._proto_
),同时这个原型也是一个对象,这个原型也会有原型,一环扣一环,就形成了一个链式的结构 ----- 原型链。
属性查找机制: 当查找对象的属性时,如果实例对象自身不存在该属性,则沿着原型链往上一级查找,找到时则输出,不存在时,则继续沿着原型链往上一级查找,直至最顶级的原型对象
Object.prototype
,如还是没找到,则输出undefined
;属性修改机制: 只会修改实例对象本身的属性,如果不存在,则进行添加该属性,如果需要修改原型的属性时,则可以用:
b.prototype.x = 2
;但是这样会造成所有继承于该对象的实例的属性发生改变。
# 继承
在 JS 中,继承通常指的便是 原型链继承,也就是通过指定原型,并可以通过原型链继承原型上的属性或者方法。
# 寄生组合式继承
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 指定对象
}
2
3
4
5
6
7
8
9
10
# 优化
const inherit = ((a, b) => {
const F = function() {};
return (a, b) => {
F.prototype = b.prototype;
a.prototype = new F();
a.prototype.constructor = a;
};
})();
2
3
4
5
6
7
8
# test code
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
// inheritPrototype(SubType, SuperType);
inherit(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
};
const a = new SubType('James',23)
a.sayName()
console.log(a.colors);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 使用 ES6 的语法糖 class / extends
# JavaScript 执行机制
# 谨记
- javascript 是一门单线程语言
- Event Loop 是 javascript 的执行机制
# 因果
- 因为单线程,所以 js 任务要一个一个顺序执行
- 有些任务耗时过长,后一个任务必须等着,体验不好
- 因此将任务分为:同步任务、异步任务
- 再细分任务
- macro-task(宏任务):包括整体代码script,setTimeout,setInterval
- micro-task(微任务):Promise,process.nextTick
js 执行机制是 Event Loop 事件循环,先顺序执行同步任务即宏任务,碰到异步任务按其分类分别丢入宏任务队列和微任务队列中,等该轮宏任务执行完后顺序执行微任务,第一轮循环结束;然后再顺序执行下一个宏任务,如此往复
# Node 事件循环
循环之前,会执行以下操作:
- 同步任务
- 发出异步请求
- 规划定时器生效的时间
- 执行process.nextTick()
开始循环 循环中进行的操作:
- 清空timers队列,清空nextTick队列,清空microTask队列
- 清空I/O队列,清空nextTick队列,清空microTask队列
- 清空check队列,清空nextTick队列,清空microTask队列
- 清空close队列,清空nextTick队列,清空microTask队列
# v8引擎中一段js代码如何执行的
在执行一段代码时,JS 引擎会首先创建一个执行栈
然后JS引擎会创建一个全局执行上下文,并push到执行栈中, 这个过程JS引擎会为这段代码中所有变量分配内存并赋一个初始值(undefined),在创建完成后,JS引擎会进入执行阶段,这个过程JS引擎会逐行的执行代码,即为之前分配好内存的变量逐个赋值(真实值)。
如果这段代码中存在function的声明和调用,那么JS引擎会创建一个函数执行上下文,并push到执行栈中,其创建和执行过程跟全局执行上下文一样。但有特殊情况,即当函数中存在对其它函数的调用时,JS引擎会在父函数执行的过程中,将子函数的全局执行上下文push到执行栈,这也是为什么子函数能够访问到父函数内所声明的变量。
还有一种特殊情况是,在子函数执行的过程中,父函数已经return了,这种情况下,JS引擎会将父函数的上下文从执行栈中移除,与此同时,JS引擎会为还在执行的子函数上下文创建一个闭包,这个闭包里保存了父函数内声明的变量及其赋值,子函数仍然能够在其上下文中访问并使用这边变量/常量。当子函数执行完毕,JS引擎才会将子函数的上下文及闭包一并从执行栈中移除。
最后,JS引擎是单线程的,那么它是如何处理高并发的呢?即当代码中存在异步调用时JS是如何执行的。比如setTimeout或fetch请求都是non-blocking的,当异步调用代码触发时,JS引擎会将需要异步执行的代码移出调用栈,直到等待到返回结果,JS引擎会立即将与之对应的回调函数push进任务队列中等待被调用,当调用(执行)栈中已经没有需要被执行的代码时,JS引擎会立刻将任务队列中的回调函数逐个push进调用栈并执行。这个过程我们也称之为事件循环。
# 数组去重
const uniq = arr => [...new Set(arr)];
const uniq = arr => arr.reduce((a, b) => (!a.includes(b) ? [...a, b] : a), []);
const uniq = arr => {
let obj = {};
return arr.filter(item =>
obj.hasOwnProperty(typeof item + item)
? false
: (obj[typeof item + item] = true)
);
};
2
3
4
5
6
7
8
9
10
11
12
# 数组扁平 flat
// 使用 flat
const flatten = arr => arr.flat(Infinity);
// 使用 reduce
const flatten = arr =>
arr.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []);
// 使用 reduce 实现 flat 可传入层数
const flatten = (arr, depth = 1) =>
depth > 0
? arr.reduce(
(a, b) => a.concat(Array.isArray(b) ? flatten(b, depth - 1) : b),
[]
)
: arr.slice();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 节流和防抖
# 防抖
将多次高频操作优化为只在最后一次执行,通常使用的场景是:用户输入,只需再输入完成后做一次输入校验即可。
function debounce(fn, wait, immediate) {
let timer = null;
return function(...args) {
const context = this;
if (immediate && !timer) {
fn.apply(context, args);
timer = 1;
} else {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context, args);
}, wait);
}
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 节流
每隔一段时间后执行一次,也就是降低频率,将高频操作优化成低频操作,通常使用场景: 滚动条事件 或者 resize 事件,通常每隔 100~500 ms执行一次即可。
function throttle(fn, wait, immediate) {
let timer = null;
let callnow = immediate;
return function(...args) {
const context = this;
if (callnow) {
fn.apply(context, args);
callnow = false;
}
if (!timer) {
timer = setTimeout(() => {
fn.apply(context, args);
timer = null;
}, wait);
}
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Promise
# 基本用法
var promise = new Promise((resolve, reject) => {
if (操作成功) {
resolve(value);
} else {
reject(error);
}
});
promise.then(
value => {
// success
},
value => {
// failure
}
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Promise
对象是一个构造函数,用来生成Promise
实例。Promise
构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。resolve
函数的作用是,将Promise
对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject
函数的作用是,将Promise
对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。- 注意,调用
resolve
或reject
并不会终结 Promise 的参数函数的执行。
Promise
实例生成以后,可以用then
方法分别指定resolved
状态和rejected
状态的回调函数。Promise
新建后就会立即执行。
# 方法
Promise.prototype.then()
- Promise 实例具有
then
方法,也就是说,then
方法是定义在原型对象Promise.prototype
上的。 - 它的作用是为 Promise 实例添加状态改变时的回调函数。
then
方法返回的是一个新的Promise
实例(不是原来那个Promise
实例)。因此可以采用链式写法。
- Promise 实例具有
Promise.prototype.catch()
Promise.prototype.catch
方法是.then(null, rejection)
或.then(undefined, rejection)
的别名,用于指定发生错误时的回调函数。- Promise 会吃掉错误 -- Promise 内部的错误不会影响到 Promise 外部的代码
Promise.prototype.finally()
finally
方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。
Promise.resolve() / Promise.reject()
作用是将现有对象转为 Promise 对象,参数可为空
-
Promise.all
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。- 只有这 n 个实例的状态都变成
fulfilled
,或者其中有一个变为rejected
,才会调用Promise.all
方法后面的回调函数。 - 如果作为参数的 Promise 实例,自己定义了
catch
方法,那么它一旦被rejected
,并不会触发Promise.all()
的catch
方法。
Promise.race()
Promise.race
方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。- 只要这 n 个实例之中有一个实例率先改变状态,就会调用
Promise.race
方法后面的回调函数。
// 回调函数没有执行 function timeoutPromise(delay) { return new Promise(function(resolve, reject) { setTimeout(function() { reject("Timeout!"); }, delay); }); } Promise.race([foo(), timeoutPromise(3000)]).then( function() {}, function(err) {} );
1
2
3
4
5
6
7
8
9
10
11
12
13
# 缺点
- 首先,无法取消
Promise
,一旦新建它就会立即执行,无法中途取消。 - 其次,如果不设置回调函数,
Promise
内部抛出的错误,不会反应到外部。 - 第三,当处于
pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
# 手写版
Promise
// 先定义三个常量
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'
// 声明构造函数
function MyPromise(fn) {
// 可能会在回调函数中使用this,就先用_this保存
const _this = this
// 初始状态是 等待中
_this.state = PENDING
// 用于保存 resolve 或 reject 中传入的值
_this.value = null
// 下面两个用于保存 then 中的回调,因为执行完 Promise 时状态可能还是等待中,应该把 then 中的回调保存起来用于状态改变时使用
_this.resolvedCbs = []
_this.rejectedCbs = []
function resolve(value) {
// 只有状态是等待中的才能改变 Promise 的状态
if (_this.state === PENDING) {
// 将状态改为完成
_this.state = RESOLVED
// 将 value 保存
_this.value = value
// 执行 then 中保存的回调
_this.resolvedCbs.forEach(cb => cb(_this.value))
}
}
function reject(value) {
// 只有状态是等待中的才能改变 Promise 的状态
if (_this.state === PENDING) {
// 将状态改为完成
_this.state = REJECTED
// 将 value 保存
_this.value = value
// 执行 then 中保存的回调
_this.rejectedCbs.forEach(cb => cb(_this.value))
}
}
// 在 try catch 中调用传入的 fn 函数
try {
// 将 resolve 和 reject 当参数传进去
fn(resolve, reject)
} catch (e) {
// 执行函数过程中出现错误就捕获
reject(e)
}
}
MyPromise.prototype.then = function(onFulfilled, onRejected) {
// 保存 this
const _this = this
// 判断参数是否为函数,不为函数的时候将值透传
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
onRejected = typeof onRejected === 'function' ? onRejected : r => { throw r }
// 如果状态是等待态的话,就往回调函数中 push 函数
if (_this.state === PENDING) {
_this.resolvedCbs.push(onFulfilled)
_this.rejectedCbs.push(onRejected)
}
// 如果状态不是等待状态的话,就去执行相应的函数
if (_this.state === RESOLVED) {
onFulfilled(_this.value)
}
if (_this.state === REJECTED) {
onRejected(_this.value)
}
}
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
Promise.all
Promise.all = promises => {
return new Promise((resolve, reject) => {
if (promises.length === 0) {
resolve([]);
} else {
let result = [];
let count = 0;
for (let i = 0; i < promises.length; i++) {
//考虑到 i 可能是 thenable 对象也可能是普通值
Promise.resolve(promises[i]).then(
data => {
result.push(data);
if (++count === promises.length) {
//所有的 promises 状态都是 fulfilled,promise.all返回的实例才变成 fulfilled 态
resolve(result);
}
},
err => {
reject(err);
return;
}
);
}
}
});
};
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
Promise.prototype.finally
Promise.prototype.finally = function(callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason =>
P.resolve(callback()).then(() => {
throw reason;
})
);
};
2
3
4
5
6
7
8
9
10
异步编程二三事 | Promise/async/Generator实现原理解析 | 9k字
# async await
async 做一件什么事情?
- 它使得你的函数的返回值必定是 promise 对象
await 在等什么?
- await等的是右侧「表达式」的结果。
[return_value] = await expression
- await会让出线程,阻塞后面的代码。
- await等的是右侧「表达式」的结果。
await 等到之后,做了一件什么事情?
如果不是 promise , await会阻塞后面的代码,先执行async外面的同步代码,同步代码执行完,再回到async内部,把这个非promise的东西,作为 await表达式的结果。
如果它等到的是一个 promise 对象,await 也会暂停async后面的代码,先执行async外面的同步代码,等着 Promise 对象 fulfilled,然后把 resolve 的参数作为 await 表达式的运算结果。
8 张图帮你一步步看清 async/await 和 promise 的执行顺序
# Set 实现(交并差)集
Set 实现交集(Intersect) 、 并集(Union) 、差集(Difference)
let set1 = new Set([1, 2, 3])
let set2 = new Set([4, 3, 2])
let intersect = new Set([...set1].filter(value => set2.has(value)))
let union = new Set([...set1, ...set2])
let difference = new Set([...set1].filter(value => !set2.has(value)))
console.log(intersect) // Set {2, 3}
console.log(union) //Set{1, 2, 3, 4}
console.log(difference) // Set {1}
2
3
4
5
6
7
8
9
# common.js 和 es6 中模块引入的区别?
CommonJS 是一种模块规范,最初被应用于 Nodejs,成为 Nodejs 的模块规范。运行在浏览器端的 JavaScript 由于也缺少类似的规范,在 ES6 出来之前,前端也实现了一套相同的模块规范 (例如: AMD),用来对前端模块进行管理。自 ES6 起,引入了一套新的 ES6 Module 规范,在语言标准的层面上实现了模块功能,而且实现得相当简单,有望成为浏览器和服务器通用的模块解决方案。但目前浏览器对 ES6 Module 兼容还不太好,我们平时在 Webpack 中使用的 export 和 import,会经过 Babel 转换为 CommonJS 规范。
在使用上的差别主要有:
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- CommonJs 是单个值导出,ES6 Module可以导出多个
- CommonJs 是动态语法可以写在判断里,ES6 Module 静态语法只能写在顶层
- CommonJs 的 this 是当前模块,ES6 Module的 this 是 undefined
# AST
抽象语法树 (Abstract Syntax Tree),是将代码逐字母解析成 树状对象 的形式。这是语言之间的转换、代码语法检查,代码风格检查,代码格式化,代码高亮,代码错误提示,代码自动补全等等的基础。