Jacleklm's Blog

JS功能与原理的实现

2019/10/15

JS 原理的实现

实现一个 Promise()

Promise 的特点:

  • new Promise 时需要传递一个函数 fn 作为执行器(执行器会立刻执行)
  • 执行器中传递了两个参数:resolve 成功的函数、reject 失败的函数(他们调用时可以接受任何值的参数 value)
  • promise 状态只能从 pending 态转到 resolved 或者 rejected,如果状态发生改变执行相应缓存队列中的任务
  • promise 实例,每个实例都有一个 then 方法,这个方法传递两个参数,一个是成功回调 onfulfilled,另一个是失败回调 onrejected
  • promise 实例调用 then 时,会判断当前状态,如果 pending,就…;如果 resolved,就让 onfulfilled 执行;如果 rejectd,就 onrejected 执行
  • promise 中可以同一个实例 then 多次,如果状态是 pengding 需要将函数存放起来 等待状态确定后 在依次将对应的函数执行 (发布订阅)

基于上述特点,实现的 Promise 如下

1
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
function MyPromise(fn) {
const that = this;
// value 变量用于保存 resolve 或者 reject 中传入的值
this.value = null;
this.status = "pending";
// 用于保存 then 中的回调(可能有多个所以是数组),因为当执行完 Promise 时状态可能还是等待中,这时候应该把 then 中的回调保存起来用于状态改变时使用
this.resolvedAry = [];
this.rejectedAry = [];

function resolve(value) {
// 首先两个函数都得判断当前状态是否为等待中
if (that.status === "pending") {
that.status === "resolved";
that.value = value;
// 遍历回调数组并执行
that.resolvedAry.map(cb => cb(that.value));
}
}
function reject(value) {
if (that.status === "pending") {
that.status = "rejected";
that.value = value;
that.rejectedAry.map(cb => cb(that.value));
}
}

// 完成以上两个函数以后,我们就该实现如何执行 Promise 中传入的函数了
try {
fn(resolve, reject);
} catch (e) {
reject(e);
}
}

// 最后我们来实现较为复杂的 then 函数。接收两个回调函数为参数
MyPromise.prototype.then = function(onfulfilled, onrejected) {
const that = this;

// 判断两个参数是否为函数类型
onfulfilled = typeof onfulfilled === "function" ? onfulfilled : f => f;
onrejected =
typeof onrejected === "function"
? onrejected
: e => {
throw e;
};

// 当状态不是等待态时,就去执行相对应的函数。如果状态是等待态的话,就往回调函数中 push 函数
if (this.status === "pending") {
this.resolvedAry.push(onfulfilled);
this.rejectedAry.push(onrejected);
}
if (this.status === "resolved") {
onfulfilled(that.value);
}
if (this.status === "rejected") {
onrejected(that.value);
}
};

实现Promise.all()

Promise.all 接收一个 promise 对象的数组作为参数,当这个数组里的所有 promise 对象全部变为resolve或 有 reject 状态出现的时候,它才会去调用 .then 方法,它们是并发执行的。eg:

1
2
3
4
5
6
var p1 = Promise.resolve(1),
p2 = Promise.resolve(2),
p3 = Promise.resolve(3); // 若任一 Promise 状态是reject,则无法执行 .then
Promise.all([p1, p2, p3]).then(function (results) {
console.log(results); // [1, 2, 3]
});

总结 promise.all 的特点

  • 接收一个 Promise 实例的数组或具有 Iterator 接口的对象
  • 如果元素不是 Promise 对象,则使用 Promise.resolve 转成 Promise 对象
  • 如果全部成功,状态变为 resolved,返回值将组成一个数组传给回调
  • 只要有一个失败,状态就变为 rejected,返回值将直接传递给回调all() 的返回值也是新的 Promise 对象

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function promiseAll(promises) {
return new Promise(function(resolve, reject) {
if (!isArray(promises)) {
return reject(new TypeError('arguments must be an array'));
}
var resolvedCounter = 0;
var promiseNum = promises.length;
var resolvedValues = new Array(promiseNum);
for (var i = 0; i < promiseNum; i++) {
(function(i) {
Promise.resolve(promises[i]).then(function(value) {
resolvedCounter++
resolvedValues[i] = value
if (resolvedCounter == promiseNum) {
return resolve(resolvedValues)
}
}, function(reason) {
return reject(reason)
})
})(i)
}
})
}

手写实现 call(), apply(), bind()

call()

  • 首先 context 为可选参数,如果不传的话默认上下文为 window
  • 接下来给 context 创建一个 fn 属性,并将值设置为需要调用的函数
  • 因为 call 可以传入多个参数作为调用函数的参数,所以需要将参数剥离出来
  • 然后调用函数并将对象上的函数删除
1
2
3
4
5
6
7
8
9
10
11
Function.prototype.myCall = function(context) {
if (typeof this !== "function") {
throw new TypeError("Error");
}
context = context || window;
context.fn = this;
const args = [...arguments].slice(1);
const result = context.fn(...args);
delete context.fn;
return result;
};

apply()

类似,区别在于对参数的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
};

bind()

bind 返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过 new 的方式

  • 对于直接调用来说,这里选择了 apply 的方式实现,但是对于参数需要注意以下情况:因为 bind 可以实现类似这样的代码 f.bind(obj, 1)(2),所以我们需要将两边的参数拼接起来,于是就有了这样的实现 args.concat(...arguments)
  • 通过 new 的方式,在之前的章节中我们学习过如何判断 this,对于 new 的情况来说,不会被任何方式改变 this,所以对于这种情况我们需要忽略传入的 this。(似乎这种 new 的用法很少见)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Function.prototype.myBind = function(context) {
if (typeof this !== "function") {
throw new TypeError("Error");
}
const _this = this;
const args = [...arguments].slice(1);
// 返回一个函数
return function F() {
// 因为返回了一个函数,我们可以 new F(),所以需要判断
if (this instanceof F) {
return new _this(...args, ...arguments);
}
// 直接调用的情况
return _this.apply(context, args.concat(...arguments));
};
};

instanceof

instanceof 可以正确的判断对象的类型,其内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。实现原理如下:

  • 首先获取类型的原型
  • 然后获得对象的原型
  • 然后一直循环判断对象的原型是否等于类型的原型,直到对象原型为 null,因为原型链最终为 null
1
2
3
4
5
6
7
8
9
function muInstanceof(left, right) {
let prototype = right.prototype;
left = left.__proto__;
while (true) {
if (left === null || left === undefined) return false;
if (prototype === left) return true;
left = left.__proto__;
}
}

手写实现 new

在调用 new 的过程中会发生四件事情:

  • 新生成了一个对象
  • 链接到原型
  • 绑定 this(使用构造函数的 this)
  • 返回新对象(原始类型的话忽略,如果是引用类型的话就返回这个对象)
1
2
3
4
5
6
7
Function.prototype.myNew() = function(func, ...args) {
// func是new后面的那个构造函数
let obj = {};
obj._proto_ = func.prototype;
let result = func.apply(obj, args);
return result instanceof Object ? result : obj;
};

手写实现 Object.create()

Object.create()方法创建一个新对象,使用参数对象(而不是参数对象.prtototype)来提供新创建的对象的__proto__

1
2
3
4
5
Object.prototype.mycreate = function(obj) {
function F() {}
F.prototype = obj;
return new F();
};

0.1 +0.2 !== 0.3

对于纯小数来说,十进制的 0.375 会被存储为: 0.011 其代表 (1/2)^2 + (1/2)^3 = 1/4 + 1/8 = 0.375
但是对 0.1 这种数值来说,算下来是 0.000110011… 由于存储空间有限,最后计算机会舍弃后面的数值,所以我们最后就只能得到一个近似值。JS 采用 IEEE 754 双精度版本(64 位)浮点数标准,也是只能取得一个近似值。除了那些能表示成 (x/2)^n 的数可以被精确表示以外,其余小数都是以近似值得方式存在的。

1
2
3
4
5
console.log(0.1000000000000001);
// 0.1000000000000001 (中间14个0,会打印出它本身)

console.log(0.10000000000000001);
// 0.1 (中间15个0,js会认为这两个值足够接近,所以会显示0.1)
1
2
3
0.100000000000000002 === 0.1; // true (16个0)
0.200000000000000002 === 0.2; // true
0.1 + 0.2 === 0.30000000000000004; // true。此时对于JS来说,其不够近似于0.3,于是就出现了0.1 + 0.2 != 0.3 这个现象

解决:

1
parseFloat((0.1 + 0.2).toFixed(10)) === 0.3; // true  toFixed()将数值格式化为字符串

双向数据绑定实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//双向数据绑定
let obj = {};
let input = document.getElementById("input");
let span = document.getElementById("span");

//数据劫持
Object.defineProperty(obj, "text", {
configurable: true,
enumerable: true,
get() {
console.log("获取数据");
},
set(newVal) {
console.log("数据更新");
input.value = newVal; // 要双向绑定所以有这句
span.innerHTML = newVal;
}
});

//监听
input.addEventListener("keyup", function(e) {
obj.text = e.target.value;
});

JS 功能的实现

数组去重

1
let arr = [1, 5, 8, 3, 7, 5, 1, 8, 100];
  • sort() + reduce
1
2
3
4
5
6
let result = arr.sort().reduce((acc, curr) => {
if (acc.length === 0 || curr !== acc[acc.length - 1]) {
acc.push(curr);
}
return acc;
}, []);
  • Array.from(new Set(arr))
1
let result = Array.from(new Set(arr));
  • for…of + Object (性能最优)。利用对象的属性不会重复这一特性,校验数组元素是否重复
1
2
3
4
5
6
7
8
let result = [];
obj = {};
for (i of arr) {
if (!obj[i]) {
result.push(i);
obj[i] = 1;
}
}

数组扁平化

1
let arr = (arr = [1, [2, [3, [4, 5, , "tset"]]], 6]);
  • Array.prototype.flat()。参数是要提取的嵌套数组的深度(数字),默认值是 1;也可以直接写 Infinity 作为参数。但是会移除数组中的空项
1
let result = arr.flat(Infinity); // [1, 2, 3, 4, 5, "tset", 6]
  • reduce 写一个递归函数。似乎也会移除空项
1
2
3
4
5
6
7
function myFlat(arr) {
return arr.reduce(
(acc, curr) => acc.concat(Array.isArray(curr) ? myFlat(curr) : curr),
[]
);
}
// [1, 2, 3, 4, 5, "tset", 6]
  • 扩展运算符 + Array.prototype.some() 这里的 isArray 用法稍微有点特殊。并且空项不会被移除,会保留为undefined
1
2
3
while (arr.some(Array.isArray)) {
arr = [].concat(...arr);
}

函数的 arguement 怎么转数组

Array.from(), 扩展运算符

for…in, for…of, forEach()

for…in

一般用来遍历对象。有几个缺点:

  • for…in 会遍历手动添加的原型链上的键。比如你自己写个Object.prototype.test = function { xxx },是能遍历到这个test属性的
  • 用来遍历数组的时候是遍历数组的键名(index),并且这个 index 是字符串
  • 某些情况下,for…in 循环会以任意顺序遍历键名

for…of

  • 遍历数组得到数组的 value
  • for (let [k, v] of Object.entries(obj)) { } 用来遍历对象,能获取对象的键和键值
1
2
3
4
5
6
7
8
9
let obj = {
name: "jacle",
age: 22,
job: "student"
};
let arr = ["jacle", 22, "student"];
for (let [key, value] of Object.entries(obj)) {
console.log(key, ":", value);
}

forEach()

遍历数组,问题是无法用 break,return 跳出循环
如何中断 forEach 循环:

  • 使用 try 监视代码块,在需要中断的地方抛出异常
  • 官方推荐方法(替换方法):用 every 和 some 替代 forEach 函数。every 在碰到 return false 的时候,中止循环。some 在碰到 return ture 的时候,中止循环

数组中是否包含某个值

  • Array.prototype.indexOf(值)。返回 index 或 -1 。该方法也可用于 String
  • Array.prototype.includes(值)。返回布尔值。该方法也可用于 String
  • Array.prototype.find(callback[,thisVal])。返回数组中满足条件的第一个元素的值,如果没有,返回 undefined
1
2
let arr = [1, 2, 3, 4];
let result = arr.find(item => item > 100); // 4
  • Array.prototype.findeIndex(callback[,thisArg])。返回数组中满足条件的第一个元素的下标,如果没有找到,返回-1]

对象按规则排序

有一个数组,里面都是对象,现在要针对对象中的某一个 key 进行排序,顺序是已给定的数组。
比如原数组为[{a:'ww'},{a:'ff'},{a:'pe'}]
顺序是[{ww:1},{pe:3},{hf:2},{oo:4},{ff:5}]
那么输出是 [{a:'ww'},{a:'pe'},{a:'ff'}]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var objarr = [{ a: "ww" }, { a: "ff" }, { a: "pe" }];
var rulearr = [{ ww: 1 }, { pe: 3 }, { hf: 2 }, { oo: 4 }, { ff: 5 }];

//暴力解决
function sortByRule(objarr, key, rulearr) {
let ResultArr = [];
rulearr.forEach(item => {
//获取当前的value和规则的位次
let value = Object.getOwnPropertyNames(item)[0];
let order = item[value];
//找到对应的obj放入对应位次的位置
ResultArr[order] = objarr.find(item => item[key] === value);
});
//去掉那些为空的
return ResultArr.filter(item => item);
}

n 到 m 范围的随机整数数

  • Math.random()生成[0, 1)的数,所以
1
Math.random() * m; // 生成{0,m)的数;
  • 要整数有向下取整 Math.floor(),向上 Math.ceil()。所以[1, m]的数是
1
2
3
Math.ceil(Math.random() * m); // [1, m]的数
Math.floor(Math.random() * m) + 1; // [1, m]的数
Math.floor(Math.random() * m); // [0, m-1]的数
  • 所以希望生成[n, m]的随机数
1
Math.floor(Math.random() * (max - min + 1) + min);

深拷贝与浅拷贝的实现

赋值(=),浅拷贝与深拷贝的区别

浅拷贝

浅拷贝只复制一层对象的属性,并不包括对象里面的为引用类型的数据

  • Object.assign 只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,是浅拷贝

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。Object.assign(target, source)。直接用 Object.assign(target, source) 这种用法就能更新target对象。当然也能用来赋值新对象

1
2
3
4
5
6
let a = {
age: 1
};
let b = Object.assign({}, a);
a.age = 2;
console.log(b.age); // 1
  • 展开运算符 …
1
2
3
4
5
6
let a = {
age: 1
};
let b = { ...a };
a.age = 2;
console.log(b.age); // 1
  • 数据的浅拷贝还有 let arr2 = [].concat(arr1)let arr2 = arr1.slice()

深拷贝

对对象以及对象的所有子对象进行拷贝
通过 JSON.parse(JSON.stringify(object))来实现

1
2
3
4
5
6
7
8
9
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;会忽略 symbol;不能序列化函数;不能解决循环引用的对象
自己实现一个深拷贝的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
function deepClone(obj) {
// 判断传进来的obj是否是引用类型
function isObj(o) {
return (typeof o === "object" || typeof o === "function") & (o !== null);
}
// 判断传进来的obj这个引用类型是对象还是数组,按正确的方式浅拷贝一次
let newobj = Array.isArray(obj) ? [...obj] : { ...obj };
// 对新对象的key遍历,原来obj的key是基本类型还是引用类型,基本类型就直接复制,引用类型就深拷贝一次
Reflect.ownKeys(newobj).forEach(key => {
newobj[key] = isObj(obj[key]) ? deepClone(obj[key]) : obj[key];
});
return newobj;
}

时间戳的获取

时间戳是指格林威治时间1970年01月01日00时00分00秒起至当下的总秒数。JS中拿到时间戳:

1
2
3
4
5
6
7
8
9
10
11
第一种方法:
var timestamp = Date.parse(new Date());

第二种方法:
var timestamp = (new Date()).valueOf();

第三种方法:
var timestamp = new Date().getTime();

第四种方法:
var timestamp = Date.now();

参考资料
实现 JavaScript 异步方法 Promise.all

CATALOG
  1. 1. JS 原理的实现
    1. 1.1. 实现一个 Promise()
    2. 1.2. 实现Promise.all()
    3. 1.3. 手写实现 call(), apply(), bind()
      1. 1.3.1. call()
      2. 1.3.2. apply()
      3. 1.3.3. bind()
    4. 1.4. instanceof
    5. 1.5. 手写实现 new
    6. 1.6. 手写实现 Object.create()
    7. 1.7. 0.1 +0.2 !== 0.3
    8. 1.8. 双向数据绑定实现原理
  2. 2. JS 功能的实现
    1. 2.1. 数组去重
    2. 2.2. 数组扁平化
    3. 2.3. 函数的 arguement 怎么转数组
    4. 2.4. for…in, for…of, forEach()
      1. 2.4.1. for…in
      2. 2.4.2. for…of
      3. 2.4.3. forEach()
    5. 2.5. 数组中是否包含某个值
    6. 2.6. 对象按规则排序
    7. 2.7. n 到 m 范围的随机整数数
    8. 2.8. 深拷贝与浅拷贝的实现
      1. 2.8.1. 赋值(=),浅拷贝与深拷贝的区别
      2. 2.8.2. 浅拷贝
      3. 2.8.3. 深拷贝
      4. 2.8.4. 时间戳的获取