主题
JavaScript 进阶
1. 内置类型
JS
中分为七种内置类型,七种内置类型又分为两大类型:基本类型和对象(Object
) 。- 基本类型有六种:
null
,undefined
,boolean
,number
,string
,symbol
。 - 其中
JS
的数字类型是浮点类型的,没有整型 。并且浮点类型基于IEEE 754
标准实现,在使用中会遇到某些Bug
。NaN
也属于number
类型, 并且NaN
不等于自身 。 - 对于基本类型来说, 如果使用字面量的方式,那么这个变量只是个字面量, 只有在必要的时候才会转换为对应的类型。
js
let a = 111; // 这只是字面量,不是 number 类型
a.toString(); // 使用时候才会转换为对象类型
对象 ( Object
) 是引用类型,在使用过程中会遇到浅拷贝和深拷贝的问题。
js
let a = { name: "FE" };
let b = a;
b.name = "EF";
console.log(a.name); // EF
2. Typeof
typeof
对于基本类型, 除了 null
都可以显示正确的类型
js
typeof 1; // ' number'
typeof "1"; // 'string'
typeof undefined; // 'undefined'
typeof true; // 'boolean'
typeof Symbol(); // 'symbol'
typeof b; // b 没有声明,但是还会显示 undefined
typeof
对于对象, 除了函数都会显示 object
js
typeof []; // 'object' typeof {} // 'object'
typeof console.log; // 'function'
对于 null
来说, 虽然它是基本类型,但是会显示 object
, 这是⼀个存在很久了的 Bug
js
typeof null; // 'object'
PS:为什么会出现这种情况呢? 因为在
JS
的最初版本中,使用的是32
位系统, 为了性能考虑使用低位存储了变量的类型信息,000
开头代表是对象,然而null
表示为全零,所以将它错误的判断为object
。虽然现在的内部类型判断代码已经改变了,但是对于这个Bug
却是⼀直流传下来。
如果我们想获得⼀个变量的正确类型, 可以通过 Object.prototype.toString.call(xx)
。这样我们就可以获得类似 [object Type]
的字符串
js
let a;
// 我们也可以这样判断 undefined
a === undefined;
// 但是 undefined 不是保留字,能够在低版本浏览器被赋值
let undefined = 1;
// 这样判断就会出错
// 所以可以用下面的方式来判断,并且代码量更少
// 因为 void 后面随便跟上⼀个组成表达式
// 返回就是 undefined
a === void 0;
3. 类型转换
- 转
Boolean
在条件判断时, 除了 undefined
, null
, false
, NaN
, '' ,0
, -0
, 其他所有值都转为 true
, 包括所有对象
- 对象转基本类型
对象在转换基本类型时, 首先会调用 valueOf
然后调用 toString
。并且这两个方法你是可以重写的
js
let a = {
valueOf() {
return 0;
},
};
- 四则运算符
只有当加法运算时, 其中⼀方是字符串类型,就会把另⼀个也转为字符串类型 。其他运算只要其中⼀方是数字,那么另⼀方就转为数字 。并且加法运算会触发三种类型转换:将值转换为原始值,转换为数字,转换为字符串
js
1 + "1"; // '11'
2 * "2"; // 4
[1, 2] + [2, 1]; // '1,22,1'
// [1, 2].toString() -> '1,2'
// [2, 1].toString() -> '2,1'
// '1,2' + '2,1' = '1,22,1'
对于加号需要注意这个表达式 'a' + + 'b'
js
"a" + +"b"; // -> "aNaN"
// 因为 + 'b' -> NaN
// 你也许在⼀些代码中看到过 + '1' -> 1
==
操作符 这里来解析⼀道题目[] == ![] // -> true
,下面是这个表达式为何为true
的步骤
js
// [] 转成 true, 然后取反变成 false
[] == false
// 根据第 8 条得出
[] == ToNumber(false)
[] == 0
// 根据第 10 条得出
ToPrimitive( []) == 0
// [].toString() -> ''
'' == 0
// 根据第 6 条得出
0 == 0 // -> true
比较运算符
- 如果是对象,就通过
toPrimitive
转换对象 - 如果是字符串,就通过
unicode
字符索引来比较
- 如果是对象,就通过
4. 原型
- 每个函数都有
prototype
属性, 除了Function.prototype.bind()
,该属性指向原型。 - 每个对象都有
__proto__
属性, 指向了创建该对象的构造函数的原型 。其实这个属性指 向了[[prototype]]
,但是[[prototype]]
是内部属性, 我们并不能访问到,所以使 用_proto_
来访问。 - 对象可以通过
__proto__
来寻找不属于该对象的属性,__proto__
将对象连接起来组成了原型链。
5. new
- 新生成了⼀个对象
- 链接到原型
- 绑定
this
- 返回新对象
在调用 new
的过程中会发生以上四件事情, 我们也可以试着来自己实现⼀个 new
js
function create() {
// 创建⼀个空的对象
let obj = new Object();
// 获得构造函数
let Con = [].shift.call(arguments);
// 链接到原型
obj.__proto__ = Con.prototype;
// 绑定 this,执行构造函数
let result = Con.apply(obj, arguments);
// 确保 new 出来的是个对象
return typeof result === "object" ? result : obj;
}
6. instanceof
instanceof
可以正确的判断对象的类型, 因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype
,我们也可以试着实现⼀下 instanceof
js
function instanceof(left, right) {
// 获得类型的原型
let prototype = right.prototype
// 获得对象的原型
left = left.__proto__
// 判断对象的类型是否等于类型的原型
while (true) {
if (left === null)
return false
if (prototype === left)
return true
left = left.__proto__
}
}
7. this
js
function foo() {
console.log(this.a);
}
var a = 1;
foo();
var obj = {
a: 2,
foo: foo,
};
obj.foo();
// 以上两者情况 `this` 只依赖于调用函数前的对象,优先级是第⼆个情况大于第⼀个情况
// 以下情况是优先级最高的, `this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向
var c = new foo();
c.a = 3;
console.log(c.a);
// 还有种就是利用 call, apply, bind 改变 this, 这个优先级仅次于 new
看看箭头函数中的 this
js
function a() {
return () => {
return () => {
console.log(this);
};
};
}
console.log(a()()());
箭头函数其实是没有 this
的, 这个函数中的 this
只取决于他外面的第⼀个不是箭头函数的函数的 this
。在这个例子中, 因为调用 a
符合前面代码中的第⼀个情况,所以 this
是 window
。并且 this
⼀旦绑定了上下文,就不会被任何代码改变
8. 执行上下文
当执行
JS
代码时,会产生三种执行上下文- 全局执行上下文
- 函数执行上下文
eval
执行上下文
每个执行上下文中都有三个重要的属性
- 变量对象 (
VO
), 包含变量 、函数声明和函数的形参,该属性只能在全局上下文中访问 - 作用域链 (
JS
采用词法作用域,也就是说变量的作用域是在定义时就决定了) this
jsvar a = 10; function foo(i) { var b = 20; } foo();
- 变量对象 (
对于上述代码,执行栈中有两个上下文:全局上下文和函数
foo
上下文。
js
stack = [globalContext, fooContext];
- 对于全局上下文来说,
VO
大概是这样的
js
globalContext. VO === globe
globalContext.VO = {
a: undefined,
foo: <Function>,
}
- 对于函数
foo
来说,VO
不能访问, 只能访问到活动对象 (AO
)
js
fooContext. VO = = = foo. AO
fooContext.AO {
i: undefined,
b: undefined,
arguments: <>
}
// arguments 是函数独有的对象(箭头函数没有)
// 该对象是⼀个伪数组,有 `length` 属性且可以通过下标访问元素
// 该对象中的 `callee` 属性代表函数本身
// `caller` 属性代表函数的调用者
- 对于作用域链, 可以把它理解成包含自身变量对象和上级变量对象的列表, 通过
[[Scope]]
属性查找上级变量
js
fooContext. [[Scope]] = [
globalContext.VO
]
fooContext.Scope = fooContext. [[Scope]] + fooContext.VO
fooContext.Scope = [
fooContext.VO,
globalContext.VO
]
- 接下来让我们看⼀个老生常谈的例子,
var
js
b(); // call b
console.log(a); // undefined
var a = "Hello world";
function b() {
console.log("call b");
}
想必以上的输出大家肯定都已经明白了, 这是因为函数和变量提升的原因 。通常提升的解释是说将声明的代码移动到了顶部, 这其实没有什么错误,便于大家理解 。但是更准确的解释应该是:在生成执行上下文时,会有两个阶段 。第⼀个阶段是创建的阶段 ( 具体步骤是创建 VO
), JS
解释器会找出需要提升的变量和函数, 并且给他们提前在内存中开辟好空间, 函数的话会将整个函数存⼊内存中, 变量只声明并且赋值为 undefined
,所以在第⼆个阶段,也就是代码执行阶段, 我们可以直接提前使用。
- 在提升的过程中,相同的函数会覆盖上⼀个函数, 并且函数优先于变量提升
js
b(); // call b second
function b() {
console.log("call b fist");
}
function b() {
console.log("call b second");
}
var b = "Hello world";
var
会产生很多错误,所以在 ES6
中引⼊了 let
。 let
不能在声明前使用,但是这并不是常说的 let
不会提升, let
提升了声明但没有赋值, 因为临时死区导致了并不能在声明前使用。
- 对于非匿名的立即执行函数需要注意以下⼀点
js
var foo = 1(
(function foo() {
foo = 10;
console.log(foo);
})()
); // -> ƒ foo() { foo = 10 ; console.log(foo) }
因为当 JS
解释器在遇到非匿名的立即执行函数时,会创建⼀个辅助的特定对象,然后将函数名称作为这个对象的属性, 因此函数内部才可以访问到 foo
,但是这个值又是只读的,所以对它的赋值并不生效,所以打印的结果还是这个函数, 并且外部的值也没有发生更改。
js
specialObject = { } ;
Scope = specialObject + Scope;
foo = new FunctionExpression;
foo. [ [Scope]] = Scope;
specialObject.foo = foo; // {DontDelete}, {ReadOnly}
delete Scope[0]; // remove specialObject from the front of scope chain
9. 闭包
闭包的定义很简单: 函数 A
返回了⼀个函数 B
, 并且函数 B
中使用了函数 A
的变量, 函数 B
就被称为闭包。
js
function A() {
let a = 1;
function B() {
console.log(a);
}
return B;
}
你是否会疑惑, 为什么函数 A
已经弹出调用栈了, 为什么函数 B
还能引用到函数 A
中的变量 。因为函数 A
中的变量这时候是存储在堆上的 。现在的 JS
引擎可以通过逃逸分析辨别出哪些变量需要存储在堆上, 哪些需要存储在栈上。
经典面试题,循环中使用闭包解决 var 定义函数的问题
js
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
首先因为 setTimeout
是个异步函数,所有会先把循环全部执行完毕, 这时候 i
就是 6
了,所以会输出⼀堆 6
。
解决办法两种, 第⼀种使用闭包
js
for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
第⼆种就是使用 setTimeout
的第三个参数
js
for (var i = 1; i <= 5; i++) {
setTimeout(
function timer(j) {
console.log(j);
},
i * 1000,
i
);
}
第三种就是使用 let
定义 i
了
js
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
因为对于 let
来说,他会创建⼀个块级作用域,相当于
js
{ // 形成块级作用域
let i = 0
{
let ii = i
setTimeout(function timer() {
console.log( i );
}, i*1000 );
}
i++
{
let ii = i
}
i++
{
let ii = i
}
...
}
10. 深浅拷贝
js
let a = {
age: 1,
};
let b = a;
a.age = 2;
console.log(b.age); // 2
从上述例子中我们可以发现, 如果给⼀个变量赋值⼀个对象,那么两者的值会是同⼀个引用, 其中⼀方改变, 另⼀方也会相应改变。
通常在开发中我们不希望出现这样的问题, 我们可以使用浅拷贝来解决这个问题
浅拷贝
- 首先可以通过
Object.assign
来解决这个问题
js
let a = {
age: 1,
};
let b = Object.assign({}, a);
a.age = 2;
console.log(b.age); // 1
- 当然我们也可以通过展开运算符 (
…
) 来解决
js
let a = {
age: 1,
};
let b = { ...a };
a.age = 2;
console.log(b.age); // 1
通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就需要使用到深拷贝了
js
let a = {
age: 1,
jobs: {
first: "FE",
},
};
let b = { ...a };
a.jobs.first = "native";
console.log(b.jobs.first); // native
浅拷贝只解决了第⼀层的问题, 如果接下去的值中还有对象的话,那么就又回到刚开始的话题了,两者享有相同的引用 。要解决这个问题, 我们需要引入深拷
深拷贝
- 这个问题通常可以通过
JSON.parse(JSON.stringify(object))
来解决
js
let a = {
age: 1,
jobs: {
first: "FE",
},
};
let b = JSON.parse(JSON.stringify(a));
a.jobs.first = "native";
console.log(b.jobs.first); // FE
但是该方法也是有局限性的:
- 会忽略
undefined
- 不能序列化函数
- 不能解决循环引用的对象
js
let obj = {
a: 1,
b: {
c: 2,
d: 3,
},
};
obj.c = obj.b;
obj.e = obj.a;
obj.b.c = obj.c;
obj.b.d = obj.b;
obj.b.e = obj.b.c;
let newObj = JSON.parse(JSON.stringify(obj));
console.log(newObj);
如果你有这么⼀个循环引用对象,你会发现你不能通过该方法深拷贝
在遇到函数或者 undefined
的时候,该对象也不能正常的序列化
js
let a = {
age: undefined,
jobs: function () {},
name: "poetries",
};
let b = JSON.parse(JSON.stringify(a));
console.log(b); // {name: "poetries"}
你会发现在上述情况中,该方法会忽略掉函数和、 undefined
。
但是在通常情况下, 复杂数据都是可以序列化的,所以这个函数可以解决大部分问题, 并且该函数是内置函数中处理深拷贝性能最快的 。当然如果你的数据中含有以上三种情况下, 可以使用 lodash
的深拷贝函数。
11. 模块化
- 在有
Babel
的情况下, 我们可以直接使用ES6
的模块化
js
// file a.js
export function a() {}
export function b() {}
// file b.js
export default function () {}
import { a, b } from "./a.js";
import XXX from "./b.js";
- CommonJS
CommonJs
是Node
独有的规范, 浏览器中使用就需要用到Browserify
解析了。
js
// a.js
module.exports = {
a: 1,
};
// or
exports.a = 1;
// b.js
var module = require("./a.js");
module.a; // -> log 1
在上述代码中, module.exports
和 exports
很容易混淆,让我们来看看大致内部实现
js
var module = require("./a.js");
module.a;
// 这里其实就是包装了⼀层立即执行函数, 这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的⼀个变量
module.exports = {
a: 1,
};
// 基本实现
var module = {
exports: {}, // exports 就是个空对象
};
// 这个是为什么 exports 和 module.exports 用法相似的原因
var exports = module.exports;
var load = function (module) {
// 导出的东西
var a = 1;
module.exports = a;
return module.exports;
};
再来说说 module.exports
和 exports
,用法其实是相似的,但是不能对 exports
直接赋值,不会有任何效果。
对于 CommonJS
和 ES6
中的模块化的两者区别是:
- 前者支持动态导⼊ ,也就是
require(${path}/xx.js)
,后者目前不支持,但是已有提案,前者是同步导⼊, 因为用于服务端,文件都在本地, 同步导⼊即使卡住主线程影响也不大 - 而后者是异步导⼊, 因为用于浏览器, 需要下载文件, 如果也采用同步导⼊会对渲染有很大影响
- 前者在导出时都是值拷贝,就算导出的值变了, 导⼊的值也不会改变,所以如果想更新值,必须重新导⼊⼀次。
- 但是后者采用实时绑定的方式, 导⼊导出的值都指向同⼀个内存地址,所以导⼊值会跟随导出值变化
- 后者会编译成
require/exports
来执行的
- AMD
AMD
是由RequireJS
提出的
js
// AMD
define(["./a", "./b"], function (a, b) {
a.do();
b.do();
});
define(function (require, exports, module) {
var a = require("./a");
a.doSomething();
var b = require("./b");
b.doSomething();
});
12. 防抖
你是否在日常开发中遇到⼀个问题,在滚动事件中需要做个复杂计算或者实现⼀个按钮的防⼆次点击操作。
- 这些需求都可以通过函数防抖动来实现 。尤其是第⼀个需求, 如果在频繁的事件回调中做复杂计算,很有可能导致⻚⾯卡顿,不如将多次计算合并为⼀次计算, 只在⼀个精确点做操作
- PS:防抖和节流的作用都是防止函数多次调用 。区别在于,假设⼀个用户⼀直触发这个函数,且每次触发函数的间隔小于
wait
, 防抖的情况下只会调用⼀次, 而节流的情况会每隔⼀定时间 ( 参数wait
) 调用函数
js
// 这个是用来获取当前时间戳的
function now() {
return +new Date();
}
/**
* 防抖函数, 返回函数连续调用时, 空闲时间必须大于或等于 wait, func 才会执行
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗⼝的间隔
* @param {boolean} immediate 设置为ture时,是否立即调用函数
* @return {function} 返回客户调用函数
*/
function debounce(func, wait = 50, immediate = true) {
let timer, context, args;
// 延迟执行函数
const later = () =>
setTimeout(() => {
// 延迟函数执行完毕,清空缓存的定时器序号
timer = null;
// 延迟执行的情况下, 函数会在延迟函数中执行
// 使用到之前缓存的参数和上下文
if (!immediate) {
func.apply(context, args);
context = args = null;
}
}, wait);
// 这里返回的函数是每次实际调用的函数
return function (...params) {
// 如果没有创建延迟执行函数 ( later), 就创建⼀个
if (!timer) {
timer = later();
// 如果是立即执行,调用函数
// 否则缓存参数和调用上下文
if (immediate) {
func.apply(this, params);
} else {
context = this;
args = params;
}
// 如果已有延迟执行函数 ( later), 调用的时候清除原来的并重新设定⼀个
// 这样做延迟函数会重新计时
} else {
clearTimeout(timer);
timer = later();
}
};
}
对于按钮防点击来说的实现:如果函数是立即执行的,就立即调用, 如果函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行 。⼀旦我开始⼀个定时器, 只要我定时器还在,你每次点击我都重新计时 。⼀旦你点累了,定时器时间到,定时器重置为
null
,就可以再次点击了。对于延时执行函数来说的实现:清除定时器
ID
, 如果是延迟调用就调用函数
13. 节流
防抖动和节流本质是不⼀样的 。防抖动是将多次执行变为最后⼀次执行, 节流是将多次执行变成每隔⼀段时间执行
js
/**
* underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {object} options 如果想忽略开始函数的的调用,传入{leading: false}。
* 如果想忽略结尾函数的调用,传入{trailing: false}
* 两者不能共存,否则函数不能执行
* @return {function} 返回客户调用函数
*/
_.throttle = function (func, wait, options) {
var context, args, result;
var timeout = null;
// 之前的时间戳
var previous = 0;
// 如果 options 没传则设为空对象
if (!options) options = {};
// 定时器回调函数
var later = function () {
// 如果设置了 leading,就将 previous 设为 0
// 用于下面函数的第一个 if 判断
previous = options.leading === false ? 0 : _.now();
// 置空一是为了防止内存泄漏,二是为了下面的定时器判断
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function () {
// 获得当前时间戳
var now = _.now();
// 首次进入前者肯定为 true
// 如果需要第一次不执行函数
// 就将上次时间戳设为当前的
// 这样在接下来计算 remaining 的值时会大于0
if (!previous && options.leading === false) previous = now;
// 计算剩余时间
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果当前调用已经大于上次调用时间 + wait
// 或者用户手动调了时间
// 如果设置了 trailing,只会进入这个条件
// 如果没有设置 leading,那么第一次会进入这个条件
// 还有一点,你可能会觉得开启了定时器那么应该不会进入这个 if 条件了
// 其实还是会进入的,因为定时器的延时
// 并不是准确的时间,很可能你设置了2秒
// 但是他需要2.2秒才触发,这时候就会进入这个条件
if (remaining <= 0 || remaining > wait) {
// 如果存在定时器就清理掉否则会调用二次回调
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 判断是否设置了定时器和 trailing
// 没有的话就开启一个定时器
// 并且不能不能同时设置 leading 和 trailing
timeout = setTimeout(later, remaining);
}
return result;
};
};
14. 继承
在 ES5 中, 我们可以使用如下方式解决继承的问题
js
function Super() {}
Super.prototype.getNumber = function () {
return 1;
};
function Sub() {}
let s = new Sub();
Sub.prototype = Object.create(Super.prototype, {
constructor: {
value: Sub,
enumerable: false,
writable: true,
configurable: true,
},
});
以上继承实现思路就是将子类的原型设置为父类的原型
在 ES6
中, 我们可以通过 class
语法轻松解决这个问题
js
class MyDate extends Date {
test() {
return this.getTime();
}
}
let myDate = new MyDate();
myDate.test();
但是 ES6
不是所有浏览器都兼容,所以我们需要使用 Babel
来编译这段代码。
如果你使用编译过得代码调用 myDate.test()
你会惊奇地发现出现了报错
因为在
JS
底层有限制, 如果不是由Date
构造出来的实例的话, 是不能调用Date
里的函数的 。所以这也侧面的说明了:ES6
中的class
继承与ES5
中的⼀般继承写法是不同的
既然底层限制了实例必须由 Date
构造出来,那么我们可以改变下思路实现继承
js
function MyData() {}
MyData.prototype.test = function () {
return this.getTime();
};
let d = new Date();
Object.setPrototypeOf(d, MyData.prototype);
Object.setPrototypeOf(MyData.prototype, Date.prototype);
以上继承实现思路:先创建父类实例 => 改变实例原先的 __proto__
转而连接到子类的 prototype
=> 子类的 prototype
的 __proto__
改为父类的 prototype
。
通过以上方法实现的继承就可以完美解决 JS
底层的这个限制
15. call, apply, bind
call
和 apply
都是为了解决改变 this
的指向 。作用都是相同的, 只是传参的方式不同。
除了第⼀个参数外, call
可以接收⼀个参数列表, apply
只接受⼀个参数数组
js
let a = {
value: 1,
};
function getValue(name, age) {
console.log(name);
console.log(age);
console.log(this.value);
}
getValue.call(a, "yck", "24");
getValue.apply(a, ["yck", "24"]);
16. Promise 实现
Promise
是ES6
新增的语法,解决了回调地狱的问题。- 可以把
Promise
看成⼀个状态机 。初始是pending
状态, 可以通过函数resolve
和reject
,将状态转变为resolved
或者rejected
状态,状态⼀旦改变就不能再次变化。 then
函数会返回⼀个Promise
实例, 并且该返回值是⼀个新的实例而不是之前的实例 。因为Promise
规范规定除了pending
状态, 其他状态是不可以改变的, 如果返回的是⼀个相同实例的话, 多个then
调用就失去意义了 。 对于then
来说,本质上可以把它看成是flatMap
js
// 三种状态
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
// promise 接收⼀个函数参数,该函数会立即执行
function MyPromise(fn) {
let _this = this;
_this.currentState = PENDING;
_this.value = undefined;
// 用于保存 then 中的回调, 只有当 promise
// 状态为 pending 时才会缓存,并且每个实例至多缓存⼀个
_this.resolvedCallbacks = [];
_this.rejectedCallbacks = [];
_this.resolve = function (value) {
if (value instanceof MyPromise) {
// 如果 value 是个 Promise,递归执行
return value.then(_this.resolve, _this.reject);
}
setTimeout(() => {
// 异步执行,保证执行顺序
if (_this.currentState === PENDING) {
_this.currentState = RESOLVED;
_this.value = value;
_this.resolvedCallbacks.forEach((cb) => cb());
}
});
};
_this.reject = function (reason) {
setTimeout(() => {
// 异步执行,保证执行顺序
if (_this.currentState === PENDING) {
_this.currentState = REJECTED;
_this.value = reason;
_this.rejectedCallbacks.forEach((cb) => cb());
}
});
};
// 用于解决以下问题
// new Promise(() => throw Error('error))
try {
fn(_this.resolve, _this.reject);
} catch (e) {
_this.reject(e);
}
}
MyPromise.prototype.then = function (onResolved, onRejected) {
var self = this;
// 规范 2.2.7, then 必须返回⼀个新的 promise
var promise2;
// 规范 2.2.onResolved 和 onRejected 都为可选参数
// 如果类型不是函数需要忽略, 同时也实现了透传
// Promise.resolve(4).then().then((value) => console.log(value))
onResolved = typeof onResolved === "function" ? onResolved : (v) => v;
onRejected = typeof onRejected === "function" ? onRejected : (r) => throw r;
if (self.currentState === RESOLVED) {
return (promise2 = new MyPromise(function (resolve, reject) {
// 规范 2.2.4,保证 onFulfilled, onRjected 异步执行
// 所以用了 setTimeout 包裹下
setTimeout(function () {
try {
var x = onResolved(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}
if (self.currentState === REJECTED) {
return (promise2 = new MyPromise(function (resolve, reject) {
setTimeout(function () {
// 异步执行onRejected
try {
var x = onRejected(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}
if (self.currentState === PENDING) {
return (promise2 = new MyPromise(function (resolve, reject) {
self.resolvedCallbacks.push(function () {
// 考虑到可能会有报错,所以使用 try/catch 包裹
try {
var x = onResolved(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});
self.rejectedCallbacks.push(function () {
try {
var x = onRejected(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});
}));
}
};
// 规范 2.3
function resolutionProcedure(promise2, x, resolve, reject) {
// 规范 2.3.1,x 不能和 promise2 相同,避免循环引用
if (promise2 === x) {
return reject(new TypeError("Error"));
}
// 规范 2.3.2
// 如果 x 为 Promise,状态为 pending 需要继续等待否则执行
if (x instanceof MyPromise) {
if (x.currentState === PENDING) {
x.then(function (value) {
// 再次调用该函数是为了确认 x resolve 的
// 参数是什么类型,如果是基本类型就再次 resolve
// 把值传给下个 then
resolutionProcedure(promise2, value, resolve, reject);
}, reject);
} else {
x.then(resolve, reject);
}
return;
}
// 规范 2.3.3.3.3
// reject 或者 resolve 其中⼀个执行过得话, 忽略其他的
let called = false;
// 规范 2.3.3,判断 x 是否为对象或者函数
if (x !== null && (typeof x === "object" || typeof x === "function")) {
// 规范 2.3.3.2,如果不能取出 then,就 reject
try {
// 规范 2.3.3.1
let then = x.then;
// 如果 then 是函数,调用 x.then
if (typeof then === "function") {
// 规范 2.3.3.3
then.call(
x,
(y) => {
if (called) return;
called = true;
// 规范 2.3.3.3.1
resolutionProcedure(promise2, y, resolve, reject);
},
(e) => {
if (called) return;
called = true;
reject(e);
}
);
} else {
// 规范 2.3.3.4
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
// 规范 2.3.4,x 为基本类型
resolve(x);
}
}
17. Generator 实现
Generator
是 ES6
中新增的语法,和 Promise
⼀样,都可以用来异步编程
js
// 使用 * 表示这是⼀个 Generator 函数
// 内部可以通过 yield 暂停代码
// 通过调用 next 恢复执行
function* test() {
let a = 1 + 2;
yield 2;
yield 3;
}
let b = test();
console.log(b.next()); // > { value: 2, done: false }
console.log(b.next()); // > { value: 3, done: false }
console.log(b.next()); // > { value: undefined, done: true }
从以上代码可以发现,加上 *
的函数执行后拥有了 next
函数,也就是说函数执行后返回了⼀个对象 。每次调用 next 函数可以继续执行被暂停的代码 。以下是 Generator
函数的简单实现
js
// cb 也就是编译过的 test 函数
function generator(cb) {
return (function () {
var object = {
next: 0,
stop: function () {},
};
return {
next: function () {
var ret = cb(object);
if (ret === undefined) return { value: undefined, done: true };
return {
value: ret,
done: false,
};
},
};
})();
}
// 如果你使用 babel 编译后可以发现 test 函数变成了这样
function test() {
var a;
return generator(function (_context) {
while (1) {
switch ((_context.prev = _context.next)) {
// 可以发现通过 yield 将代码分割成几块
// 每次执行 next 函数就执行⼀块代码
// 并且表明下次需要执行哪块代码
case 0:
a = 1 + 2;
_context.next = 4;
return 2;
case 4:
_context.next = 6;
return 3;
// 执行完毕
case 6:
case "end":
return _context.stop();
}
}
});
}
18. Proxy
Proxy
是 ES6
中新增的功能, 可以用来自定义对象中的操作
js
let p = new Proxy(target, handler);
// `target` 代表需要添加代理的对象
// `handler` 用来自定义对象中的操作
// 可以很方便的使用 Proxy 来实现⼀个数据绑定和监听
let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property);
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
setBind(value);
return Reflect.set(target, property, value);
},
};
return new Proxy(obj, handler);
};
let obj = { a: 1 };
let value;
let p = onWatch(
obj,
(v) => {
value = v;
},
(target, property) => {
console.log(`Get '${property} ' = ${target[property]}`);
}
);
p.a = 2; // bind `value` to `2`
p.a; // -> Get 'a' = 2