夫大人者,与天地合其德,与日月合其明,与四时合其序,与鬼神合其吉凶。——《周易》
在 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 );
方案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 );
从表面看,应该是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 );
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 ;var ᅠ1 = a;var ᅠ2 = a;var ᅠ3 = 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); console .log (a == 1 ); console .log (a); console .log (a == 2 ); console .log (a); console .log (a == 3 ); console .log (a);
隐式转换 1 2 3 0 == '' 0 == [] 0 == ['' ]
原型链 所有的数组,对象,函数都有一个__proto__
,被称为隐式原型。所有的函数 都有一个prototype
属性,被称为显式原型。prototype
是函数的一个属性而已,它和原型没有绝对的关系,每个对象都有一个原型,但是只有函数才会有prototype
属性。
1 2 3 4 5 6 7 var a = function ( ){};var b=[1 ,2 ,3 ];console .log (a.prototype );console .log (b.prototype );
每个对象(实例)都有一个属性__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.getPrototypeOf
和Object.setPrototypeOf/Reflect.setPrototypeOf
(尽管如此,设置对象的原型是一个缓慢的操作,如果性能要求很高,应该避免设置对象的原型)。
使用最新的方法Object.setPrototypeOf(类似Reflect.setPrototypeOf)可以很方便地给对象设置原型,这个对象会继承该原型所有属性和方法。但是,setPrototypeOf的性能很差,我们应该尽量使用 Object.create()来为某个对象设置原型。
1 2 3 4 5 6 7 8 9 10 11 var obj={ methodA ( ){ console .log ("coffe" ); } } var newObj = Object .create (obj);newObj.methodA ();
创建对象的几种方法
对象字面量
显式使用构造函数
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} const o2 = new F ('a' ) 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 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)
面试题:在页面中创建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 ) const result = constr.apply (obj, arguments ) 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) p.sayHello ()
实现 call 函数 思路:
参考call的语法规则,需要设置一个参数thisArg,也就是this的指向;
将thisArg封装为一个Object;
通过为thisArg创建一个临时方法,这样thisArg就是调用该临时方法的对象了,会将该临时方法的this隐式指向到thisArg上
执行thisArg的临时方法,并传递参数;
删除临时方法,返回方法的执行结果。
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 Function .prototype .myCall = function (thisArg, ...arr ) { if (thisArg === null || thisArg === undefined ) { thisArg = window ; } else { thisArg = Object (thisArg); } const specialMethod = Symbol ("anything" ); thisArg[specialMethod] = this ; let result = thisArg[specialMethod](...arr); delete thisArg[specialMethod]; return result; }; let obj = { name : "coffe1891" }; function func ( ) { console .log (this .name ); } func.myCall (obj);
实现 apply 函数
传递给函数的参数处理,不太一样,其他部分跟call一样;
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 Function .prototype .myApply = function (thisArg ) { if (thisArg === null || thisArg === undefined ) { thisArg = window ; } else { thisArg = Object (thisArg); } function isArrayLike (o ) { if ( o && typeof o === "object" && isFinite (o.length ) && o.length >= 0 && o.length === Math .floor (o.length ) && o.length < 4294967296 ) 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 ; let funcForBind = function (...secondParams ) { const isNew = this instanceof funcForBind; const thisArg = isNew ? this : Object (objThis); return thisFn.call (thisArg, ...params, ...secondParams); }; 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 ){ console .log (p.name ); console .log (this .name ); console .log (secondParams); } let obj={ name :"1891" } func.myBind (obj,{name :"coffe" })("二次传参" );
实现函数重载 重载是面向对象编程语言(比如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 (() => { function overload (object, name, fn ) { var oldMethod = object[name]; object[name] = function ( ) { if (fn.length === arguments .length ) { return fn.apply (this , arguments ); } else if (typeof oldMethod === "function" ) { return oldMethod.apply (this , arguments ); } }; } function fn0 ( ) { return "no param" ; } function fn1 (param1 ) { return "1 param:" + param1; } function fn2 (param1, param2 ) { return "2 param:" + [param1, param2]; } let obj = {}; overload (obj, "fn" , fn0); overload (obj, "fn" , fn1); overload (obj, "fn" , fn2); console .log (obj.fn ()); console .log (obj.fn (1 )); console .log (obj.fn (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 ( ) { if (i === array.length ) { return Promise .resolve (); } const item = array[i++]; const p = Promise .resolve ().then (() => iteratorFn (item, array)); ret.push (p); executing.add (p); p.then (() => { executing.delete (p); }); let r = Promise .resolve (); if (executing.size >= poolLimit) { r = Promise .race (executing); } 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); });
其实所谓的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 Local<Object> global = env->context ()->Global (); 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进程。
事件循环 参考国外大神写的一系列文章
参考资料