夫大人者,与天地合其德,与日月合其明,与四时合其序,与鬼神合其吉凶。——《周易》

在 JavaScript 中, (a == 1 && a == 2 && a == 3) 是否有可能为 true

参考解决思路:a是一个对象或函数,每次调用取值都不一样,以有序的规律变化就能实现多等

方案1:使用 getter

1
2
3
4
5
6
7
let temp = 1;
Object.defineProperty(global, 'a', {
get() {
return temp++;
}
});
console.log(a === 1 && a === 2 && a === 3); // true

方案2:重写valueOf/toString

1
2
3
4
5
6
7
const a = {
value: 1,
valueOf() {
return this.value++;
}
};
console.log(a == 1 && a == 2 && a == 3); // true

从表面看,应该是valueOf()每次都被调用了,但是为什么会这样?我们又没有调用它。这里的valueOf为什么会被调用?这主要是==转换规则(换成 ===,这种方式就不成立了)

  • 如果一个是null,一个是undefined,则它们相等
  • 如果一个是数字,一个是字符串,先将字符串转换成数字,然后使用转换后的值进行比较
  • 如果其中的一个值为true,则转换成1再进行比较;如果其中一个值为false,则转换成0再进行比较
  • 如果一个值是对象,另一个值是数字或者字符串,则将对象转换成原始值再进行比较。转换成字符串时,会先调用toString(),如果没有toString()方法或者返回的不是一个原始值,则再调用valueOf(),如果还是不存在或者返回不是原始值,则会抛出一个类型错误的异常。返回的原始值会被转换成字符串;如果转换成数字时,也是类似的,不过是会先调用valueOf(),再调用toString(),返回的原始值会被转换成数字
  • 其他不同类型之间的比较均不相等

所以在这里使用 a 与这些字符进行比较时会被转换成数字,此时会默认调用字符串的valueOf()方法,我们将这个方法进行重写,用于拦截处理a的值

同理可以使用toString方法处理,因为字符串转数字类型时会涉及到valueOf()toString(),道理一样,只要符合递增规则的,a就可以实现多等,因为此a非彼a

方案3:ES6 Proxy

1
2
3
4
const a = new Proxy({i: 0}, {
get: (target, name) => name === Symbol.toPrimitive ? () => ++target.i : target[name]
});
console.log(a == 1 && a == 2 && a == 3); // true

Symbol.toPrimitive是一个内置的Symbol值,用于定义对象在被转换为原始值(如数字、字符串)时的行为。当一个对象需要被转换为原始值时,JavaScript 会调用对象的Symbol.toPrimitive方法(如果存在)。

当 a 参与 == 比较的时候,js 会尝试将 a 转化为原始值,由于 a 是一个对象,js 会调用 a 的 a[Symbol.toPrimitive] 方法,get 拦截器返回的函数会被调用,返回 target.value 的当前值,并将 value 递增。

方案4:使用不可见字符

1
2
3
4
5
var  a = 1;
var1 = a;
var2 = a;
var3 = a;
console.log( a ===ᅠ1 && a ===ᅠ2 && a ===ᅠ3 );

这些变量的名字看起来像是数字,但实际上它们的名字中包含了不可见的 Unicode 字符(例如零宽空格或其他不可见字符),这些字符在代码中不可见,但 JavaScript 引擎会将其视为变量名的一部分。

1
2
console.log(encodeURIComponent("ᅠ1")); // 输出 "%EF%BE%A01"
console.log("ᅠ1".charCodeAt(0)); // 输出 65408

方案5:join+shift

  • 对于对象数组进行比较时,这里数组a每次比较的时候都会默认调用toString(),然后toString()又会默认调用join(),这里将join()改为shift(),意思是删除第一个数组元素值并返回
  • 所以这样调用每次都会导致a数组删除第一个值并且返回删除掉的那个值,结合这样的规律,每次比较都取出对应位置的值
  • 这里是1、2、3,只要符合规律返回的值就行
1
2
3
4
5
6
7
8
9
10
var a = [1, 2, 3];
a.join = a.shift;
console.log(a); // [ 1, 2, 3, join: [Function: shift] ]
// console.log(a == 1 && a == 2 && a == 3); // true
console.log(a == 1); // true
console.log(a); // [ 2, 3, join: [Function: shift] ]
console.log(a == 2); // true
console.log(a); // [ 3, join: [Function: shift] ]
console.log(a == 3); // true
console.log(a); // [ join: [Function: shift] ]

隐式转换

1
2
3
0 == '' // true,隐式转换,字符串 -> 数字
0 == [] // true,隐式转换,对象 -> 原始值(首先调用toString,然后是valueOf,对于数组的 toString,默认是 join 方法,空数组 join 是空字符串,从而走回了字符串和数字的比较)
0 == [''] // true,和上面的情况同理

原型链

所有的数组,对象,函数都有一个__proto__,被称为隐式原型。所有的函数都有一个prototype属性,被称为显式原型。prototype是函数的一个属性而已,它和原型没有绝对的关系,每个对象都有一个原型,但是只有函数才会有prototype属性。

1
2
3
4
5
6
7
var a = function(){};
var b=[1,2,3];

//函数才有prototype属性
console.log(a.prototype);//>> function(){}
//非函数,没有prototype属性
console.log(b.prototype);//>> undefined

每个对象(实例)都有一个属性__proto__,指向他的构造函数(constructor)的prototype属性,一个对象的原型就是它的构造函数的prototype属性的值。

1
2
3
4
5
// 隐式原型等于其构造函数上的显式原型
const obj = {}
obj.__proto__ === Object.prototype
const arr = []
arr.__proto__ === Array.prototype

当一个对象的属性不存在的时候会向其构造函数的显式原型中查找(即该对象本身的隐式原型)。

对象的__proto__也有自己的__proto__,层层向上,直到__proto__为null。换句话说,原型本身也有自己的原型。这种由原型层层链接起来的数据结构成为原型链。因为null不再有原型,所以原型链的末端是null。

使用__proto__是有争议的,也不鼓励使用它。因为它从来没有被包括在EcmaScript语言规范中,但是现代浏览器都实现了它。__proto__属性已在ECMAScript 6语言规范中标准化,用于确保Web浏览器的兼容性,因此它未来将被支持。但是,它已被不推荐使用,现在更推荐使用Object.getPrototypeOf/Reflect.getPrototypeOfObject.setPrototypeOf/Reflect.setPrototypeOf(尽管如此,设置对象的原型是一个缓慢的操作,如果性能要求很高,应该避免设置对象的原型)。

使用最新的方法Object.setPrototypeOf(类似Reflect.setPrototypeOf)可以很方便地给对象设置原型,这个对象会继承该原型所有属性和方法。但是,setPrototypeOf的性能很差,我们应该尽量使用 Object.create()来为某个对象设置原型。

1
2
3
4
5
6
7
8
9
10
11
//obj的原型是Object.prototype
var obj={
methodA(){
console.log("coffe");
}
}

var newObj = Object.create(obj);//以obj为原型创建一个新的对象

//methodA实际上是newObj原型对象obj上的方法。也即newObj继承了它的原型对象obj的属性和方法。
newObj.methodA();//>> coffe

创建对象的几种方法

  1. 对象字面量
  2. 显式使用构造函数
  3. Object.create
1
2
3
4
5
6
7
8
9
10
11
12
const o1 = {name:'a'}
const o11 = new Object({name:'a'})

const F = function (name) {this.name = name} // F.prototype.constructor === F
const o2 = new F('a') // o2.__proto__ === F.prototype, F.__proto__ === Function.prototype
// F 这个函数对象是 Function 的实例
// o2 instanceof F // true
// o2 instanceof Object // true
// F.prototype.__proto__ === Object.prototype // true

const P = {name:'a'}
const o3 = Object.create(P)

只有函数有 prototype,只有对象有__proto__。函数也是对象,因此它也有 __proto__

实现继承

1
2
3
4
5
6
7
function Parent() { this.name = 'parent' }
Parent.prototype.getName = function () { return this.name }
function Child() {
Parent.call(this)
this.age = 10
}
const o = new Child()

作用域

1
2
3
4
fn1() // 不会报错,因为函数会声明前置
function fn1(){}
fn2() // 会报错,var声明前置的时候是undefined
var fn2 = function {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 闭包,函数作为参数,函数作为返回值
function F1() {
var a = 100
return function () {
console.log(a) // 自由变量,去父作用域寻找
}
}

var f1 = F1()

function F2(fn) {
var a = 200
fn()
}

F2(f1) // 100

面试题:在页面中创建10个a标签,点击的时候弹出对应的序号,一种常见的错误写法如下:

1
2
3
4
5
6
7
8
9
10
var i, a;
for (i = 0; i < 10; i++) {
a = document.createElement('a');
a.innerHTML = i + '<br>';
a.addEventListener('click', function (e) {
e.preventDefault();
alert(i);
});
document.body.appendChild(a);
}

事件监听函数中的i是自由变量,需要去父作用域去寻找,这个时候i已经是10了。

解决方案一:在原来的函数外面包一层函数,将变量作为参数传递进去:

1
2
3
4
5
6
7
8
9
10
11
12
var i, a;
for (i = 0; i < 10; i++) {
(function (i) {
a = document.createElement('a');
a.innerHTML = i + '<br>';
a.addEventListener('click', function (e) {
e.preventDefault();
alert(i);
});
document.body.appendChild(a)
})(i)
}

解决方案二:使用ES6中let的块作用域:

1
2
3
4
5
6
7
8
9
for (let i = 0; i < 10; i++) {
const a = document.createElement('a');
a.innerHTML = i + '<br>';
a.addEventListener('click', function (e) {
e.preventDefault();
alert(i);
});
document.body.appendChild(a)
}

闭包在实际开发中重要用于封装变量,收敛权限。

实现 new 运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function myNew() {
const constr = Array.prototype.shift.call(arguments) // 第一个参数是构造函数
const obj = Object.create(constr.prototype) // 创新一个新对象,该对象的原型是constr
const result = constr.apply(obj, arguments) // 改变this的指向,让this指向刚才传创建的obj
return result instanceof Object ? result : obj // 判断返回值是不是对象,如果是返回,如果不是返回刚才创建的新对象
}

function Person(name, age) {
this.name = name;
this.age = age;
}

Person.prototype.sayHello = function () {
console.log(`I am ${this.name},is ${this.age} years old!`)
}

const p = myNew(Person, '张三', 18)
console.log(p) // Person { name: '张三', age: 18 }
p.sayHello() // I am 张三,is 18 years old!

实现 call 函数

思路:

  1. 参考call的语法规则,需要设置一个参数thisArg,也就是this的指向;
  2. 将thisArg封装为一个Object;
  3. 通过为thisArg创建一个临时方法,这样thisArg就是调用该临时方法的对象了,会将该临时方法的this隐式指向到thisArg上
  4. 执行thisArg的临时方法,并传递参数;
  5. 删除临时方法,返回方法的执行结果。
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
/**
* 用原生JavaScript实现call
*/
Function.prototype.myCall = function(thisArg, ...arr) {

//1.判断参数合法性/////////////////////////
if (thisArg === null || thisArg === undefined) {
//指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
thisArg = window;
} else {
// 当以非构造函数形式被调用时,Object 等同于 new Object()。
thisArg = Object(thisArg);//创建一个可包含数字/字符串/布尔值的对象,
//thisArg 会指向一个包含该原始值的对象。
}

//2.搞定this的指向/////////////////////////
const specialMethod = Symbol("anything"); //创建一个不重复的常量
//如果调用myCall的函数名是func,也即以func.myCall()形式调用;
//根据上篇文章介绍,则myCall函数体内的this指向func
thisArg[specialMethod] = this; //给thisArg对象建一个临时属性来储存this(也即func函数)
//进一步地,根据上篇文章介绍,func作为thisArg对象的一个方法被调用,那么func中的this便
//指向thisArg对象。由此,巧妙地完成将this隐式地指向到thisArg!
let result = thisArg[specialMethod](...arr);

//3.收尾
delete thisArg[specialMethod]; //删除临时方法
return result; //返回临时方法的执行结果
};

let obj = {
name: "coffe1891"
};

function func() {
console.log(this.name);
}

func.myCall(obj);//>> coffe1891

实现 apply 函数

  1. 传递给函数的参数处理,不太一样,其他部分跟call一样;
  2. apply接受第二个参数为类数组对象, 这里用了《JavaScript权威指南》一书中判断是否为类数组对象的方法。
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
/**
* 用原生JavaScript实现apply
*/
Function.prototype.myApply = function(thisArg) {
if (thisArg === null || thisArg === undefined) {
thisArg = window;
} else {
thisArg = Object(thisArg);
}

//判断是否为【类数组对象】
function isArrayLike(o) {
if (
o && // o不是null、undefined等
typeof o === "object" && // o是对象
isFinite(o.length) && // o.length是有限数值
o.length >= 0 && // o.length为非负值
o.length === Math.floor(o.length) && // o.length是整数
o.length < 4294967296
)
// o.length < 2^32
return true;
else return false;
}

const specialMethod = Symbol("anything");
thisArg[specialMethod] = this;

let args = arguments[1]; // 获取参数数组
let result;

// 处理传进来的第二个参数
if (args) {
// 是否传递第二个参数
if (!Array.isArray(args) && !isArrayLike(args)) {
throw new TypeError(
"第二个参数既不为数组,也不为类数组对象。抛出错误"
);
} else {
args = Array.from(args); // 转为数组
result = thisArg[specialMethod](...args); // 执行函数并展开数组,传递函数参数
}
} else {
result = thisArg[specialMethod]();
}

delete thisArg[specialMethod];
return result; // 返回函数执行结果
};

实现 bind 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 * 用原生JavaScript实现bind
*/
Function.prototype.myBind = function(objThis, ...params) {
const thisFn = this;//存储调用函数,以及上方的params(函数参数)
//对返回的函数 secondParams 二次传参
let funcForBind = function(...secondParams) {
//检查this是否是funcForBind的实例?也就是检查funcForBind是否通过new调用
const isNew = this instanceof funcForBind;

//new调用就绑定到this上,否则就绑定到传入的objThis上
const thisArg = isNew ? this : Object(objThis);

//用call执行调用函数,绑定this的指向,并传递参数。返回执行结果
return thisFn.call(thisArg, ...params, ...secondParams);
};

//复制调用函数的prototype给funcForBind
funcForBind.prototype = Object.create(thisFn.prototype);
return funcForBind;//返回拷贝的函数
};
1
2
3
4
5
6
7
8
9
10
11
12
let func = function(p,secondParams){//其实测试用的func其参数可以是任意多个
console.log(p.name);
console.log(this.name);
console.log(secondParams);
}
let obj={
name:"1891"
}
func.myBind(obj,{name:"coffe"})("二次传参");
//>> coffe
//>> 1891
//>> 二次传参

实现函数重载

重载是面向对象编程语言(比如Java、C#)里的特性,JavaScript语言并不支持该特性。所谓重载(overload),就是函数名称一样,但是随着传入的参数个数不一样,调用的逻辑或返回的结果会不一样。jQuery之父John Resig曾经提供了一个非常巧妙的思路实现重载,代码如下:

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
(() => {//IIFE+箭头函数,把要写的代码包起来,避免影响外界,这是个好习惯

// 当函数成为对象的一个属性的时候,可以称之为该对象的方法。

/**
* @param {object} 一个对象,以便接下来给这个对象添加重载的函数(方法)
* @param {name} object被重载的函数(方法)名
* @param {fn} 被添加进object参与重载的函数逻辑
*/
function overload(object, name, fn) {
var oldMethod = object[name];//存放旧函数,本办法灵魂所在,将多个fn串联起来
object[name] = function () {
// fn.length为fn定义时的参数个数,arguments.length为重载方法被"调用"时的参数个数
if (fn.length === arguments.length) {//若参数个数匹配上
return fn.apply(this, arguments);//就调用指定的函数fn
} else if (typeof oldMethod === "function") {//若参数个数不匹配
return oldMethod.apply(this, arguments);//就调旧函数
//注意:当多次调用overload()时,旧函数中
//又有旧函数,层层嵌套,递归地执行if..else
//判断,直到找到参数个数匹配的fn
}
};
}

// 不传参数时
function fn0() {
return "no param";
}

// 传1个参数
function fn1(param1) {
return "1 param:" + param1;
}

// 传两个参数时,返回param1和param2都匹配的name
function fn2(param1, param2) {
return "2 param:" + [param1, param2];
}

let obj = {};//定义一个对象,以便接下来给它的方法进行重载

overload(obj, "fn", fn0);//给obj添加第1个重载的函数
overload(obj, "fn", fn1);//给obj添加第2个重载的函数
overload(obj, "fn", fn2);//给obj添加第3个重载的函数

console.log(obj.fn());//>> no param
console.log(obj.fn(1));//>> 1 param:1
console.log(obj.fn(1, 2));//>> 2 param:1,2
})();

Promise相关

怎样控制 Promise 的并发

我们知道Promise.all可以让Promise并发执行,但是这些Promise是创建的时候就开始执行了,所有数组中的Promise可以理解为同时执行,如何限制并发执行的个数呢?只能从Promise创建的时候开始考虑了。维护一个正在执行的Promise的队列记为executing,并发数记为poolLimit,当正在执行的Promise数量大于等于poolLimit就需要等待executing中一个Promise执行完腾出位置了,Promise.race提供了这样一组api:接收一个Promise数组,当第一个Promise执行完的时候,整个Promise.race执行结束。我们正好可以利用这个特性,及时腾出位置加入其它的Promise到executing中!

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
function asyncPool(poolLimit, array, iteratorFn) {
let i = 0;
const ret = [];
const executing = new Set();
const enqueue = function () {
// 边界处理,array为空数组
if (i === array.length) {
return Promise.resolve();
}
// 每调一次enqueue,初始化一个promise(使用Promise.resolve.then包装一下,这样返回的都是promise了)
const item = array[i++];
const p = Promise.resolve().then(() => iteratorFn(item, array));
// 放入promises数组
ret.push(p);
executing.add(p);
// promise执行完成后,从执行队列中删除
p.then(() => {
executing.delete(p);
});

// 使用Promise.race,每当executing队列中promise数量低于poolLimit,就实例化新的promise并执行
let r = Promise.resolve();
if (executing.size >= poolLimit) {
r = Promise.race(executing);
}
// 递归,直到遍历完array
return r.then(() => enqueue());
};
return enqueue().then(() => Promise.all(ret));
}
1
2
3
4
5
6
7
const timeout = i => new Promise(resolve => setTimeout(() => resolve(i), i));
require('util').log('start');
asyncPool(2, [1000, 5000, 3000, 2000], timeout).then(results => {
require('util').log(results);
});
// 8 Sep 17:20:07 - start
// 8 Sep 17:20:13 - [ 1000, 5000, 3000, 2000 ]

其实所谓的Promise并发控制,就是控制Promise实例化的个数。然而这样的实现效果本质上来说已经摈弃了Promise.all,期待标准库中可以提供这个功能。

如何取消 Promise

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
const makeCancelable = (promise) => {
let hasCanceled_ = false;
const wrappedPromise = new Promise((resolve, reject) => {
promise.then((val) =>
hasCanceled_ ? reject({isCanceled: true}) : resolve(val)
);
promise.catch((error) =>
hasCanceled_ ? reject({isCanceled: true}) : reject(error)
);
});
return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
},
};
};

const somePromise = new Promise(r => setTimeout(r, 1000));//创建一个异步操作
const cancelable = makeCancelable(somePromise);//为异步操作添加可取消的功能
cancelable
.promise
.then(() => console.log('resolved'))
.catch(({isCanceled, ...error}) => console.log('isCanceled', isCanceled));
// 取消异步操作
cancelable.cancel();

出处:https://github.com/crazycodeboy/RNStudyNotes/tree/master/React%20Native%20%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/React%20Native%20%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E4%B9%8B%E5%8F%AF%E5%8F%96%E6%B6%88%E7%9A%84%E5%BC%82%E6%AD%A5%E6%93%8D%E4%BD%9C

编程风格

不要混用同步和异步,容易给 debug 造成比较大的困惑,下面的代码是反面教材:

1
2
3
4
5
6
7
8
9
10
11
12
13
const cache = {};
function readFile(fileName, callback) {
if (cache[filename]) {
return callback(null, cache[filename])
}

fs.readFile(fileName, (err, fileContent) => {
if (err) return callback(err);

cache[fileName] = fileContent;
callback(null, fileContent);
});
}

改成这样是更优秀的:

1
2
3
4
5
6
7
8
9
10
11
12
13
const cache = {};
function readFile(fileName, callback) {
if (cache[filename]) {
return process.nextTick(() => callback(null, cache[filename]));
}

fs.readFile(fileName, (err, fileContent) => {
if (err) return callback(err);

cache[fileName] = fileContent;
callback(null, fileContent);
});
}

node.js源码

nodejs中的global对象什么时候初始化

global对象在src/node.cc中的被创建,在bootstrap/node.js中被初始化。在src/node.cc的LoadEnvironment方法中,有以下几行代码是用来创建global对象的。

1
2
3
4
5
6
// Add a reference to the global object
Local<Object> global = env->context()->Global();

// Expose the global object as a property on itself
// (Allows you to set stuff on `global` from anywhere in JavaScript.)
global->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "global"), global);

其中env->context()->Global()获取了当前context中的Global全局对象,global->Set(FIXED_ONE_BYTE_STRING(env->isolate(), “global”), global)将全局对象本身挂载在其global对象上,这样全局对象中有了一个global对象指向了全局对象本身,我们在该context中可以直接使用global对全局对象的引用进行访问。以上程序之后,这样我们就可以在context的任何地方对全局对象进行操作。

初始化在bootstrap/node.js中的setupGlobalVariables进行,其在上述逻辑执行后被执行,所以可以直接操作全局对象。这个函数中将process、Buffer直接放在global对象上,因此我们可以直接访问这些全局对象了。进一步地,setTimeout等定时器通过setupGlobalTimeouts方法放在global上。

为什么cluster开启多个worker的时候可以监听同一个端口不会报错

master进程内部启动了一个TCP服务器,真正监听端口的只有这个服务器,当来自前端的请求触发服务器的connection事件后,master会将对应的socket句柄发送给worker子进程。cluster内部的负载均衡均衡算法是Round-robin。master进程监听端口,将请求分发给下面的worker进程。

事件循环

参考国外大神写的一系列文章

参考资料