夫大人者,与天地合其德,与日月合其明,与四时合其序,与鬼神合其吉凶。——《周易》
在 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 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 4 5 6 7 typeof [] [] == [] typeof null typeof (()=> 1 ) typeof undefined typeof Array typeof (new Number (1 ))
根据 w3c 标准,分为基本数据类型(7)和对象(1)。基本类型: string(1), number(2), null(3), undefied(4), boolean(5), bignumber(6), symbol(7)。对象:object(1)。
数组和函数都是 object 类型。
1 2 3 4 5 6 7 8 9 10 var arr = []arr[0 ] = 1 arr['0' ] = 2 arr['aaa' ] = 3 arr['1' ] = 555 console .log (arr[0 ]) console .log (arr['0' ]) console .log (arr.length ) console .log (arr[0 ] + arr['0' ]) console .log (Object .keys (arr))
js 中的数组本质上就是对象,意味着可以用数字索引来访问元素,也可以使用字符串键来访问属性。当使用字符串键的时候,如果该字符串可以转成有效的数字索引,则将其视为数字索引,因此 arr[1]
和 arr['1']
是一个东西。数组的 length 只反映基于数字索引(也包含可以隐式转换成数字)的实际元素的数量。所以虽然 arr 对象有 3 个 key,其长度还是 2。
Object.is
用于比较两个值是否完全相等,解决了 NaN
,0
与 -0
使用 ===
存在的问题。
1 2 3 4 5 6 NaN === NaN Object .is (NaN , NaN ) 0 === -0 Object .is (0 , -0 ) Object .is (0 , +0 )
instanceof 运算符 只有对象才能使用这个运算符(基础类型不能使用),会进行原型链的查找,因此有下面的结果:
1 2 3 4 null instanceof Object 1 instanceof Number Number (1 ) instanceof Number new Number (1 ) instanceof Number
原始类型和其包装类 字符串是原始类型,它的行为和对象不同。虽然可以向字符串添加属性,但是这些属性不会持久化,并且在某些操作中会被忽略 。
1 2 3 4 5 6 7 8 const s = '123' s.c = '4' s.d = '5' console .log ('s:' , s) console .log (`s.c: ${s.c} , s.d: ${s.d} ` ); const [a,b] = s const {c,d} = sconsole .log (a,b,c,d)
s.c = '4'
, s.d = '5'
这两句话在试图给一个字符串添加属性的时候,js 会创建一个临时的对象包装器 来处理这个操作,但是这个对象包装器会在操作完成后立即被销毁。因此第 5 行输出 s.a 和 s.b 都是 undefined
。接下来第 6 行是数组的解构赋值,将字符串 s 当做一个可迭代对象 ,并逐个提取字符。因此 a:’1’, b:’2’。再接下来第 7 行使对象解构,尝试从字符串 s 中提取属性 c 和属性 d。由于字符串没有这两个属性,则 c 和 d 都是 undefined
。
如果把这个题目改一改 s 并不是原始的字符串,而是String
对象则情况如下:
1 2 3 4 5 6 7 8 const s = new String ('123' ) s.c = '4' s.d = '5' console .log ('s:' , s) console .log (`s.c: ${s.c} , s.d: ${s.d} ` ); const [a,b] = s const {c,d} = sconsole .log (a,b,c,d)
隐式转换 1 2 3 0 == '' 0 == [] 0 == ['' ]
优先级:Symbol.toPrimitive
> valueOf
> toString
1 2 3 4 5 6 7 8 9 10 11 const obj = { [Symbol .toPrimitive ](hint) { if (hint === 'number' ) return 42 ; if (hint === 'string' ) return 'foo' ; return 'default' ; } }; console .log (+obj); console .log (`${obj} ` ); console .log (obj + '' );
浮点数的精度问题 在编程的时候要慎用 toFixed
,因为它可能会欺骗我们。
1 2 3 4 console .log (2.55 .toFixed (1 )); console .log (1.45 .toFixed (1 )); console .log (1.55 .toFixed (1 )); console .log (3.55 .toFixed (1 ));
从上面的结果来看,规则非常模糊,上取整、下取整、四舍五入都有可能发生。造成以上结果的原因是:浮点数的存储、运算、显示这 3 个步骤都有可能不精确的!
存储:0.3 不精确,0.3.toPrecision(30),0.2.toPrecision(30),有的数偏小,有的偏大
运算:0.3 - 0.2,因为 0.3 存储偏小,0.2 存储偏大,减法运算会将误差进一步扩大!也有可能运算将不精确缩小,甚至没有了! 0.3+0.2 = 0.5
显示:会在一定误差内做近似处理
浮点数的整数位数占用的位数会影响小数部分的精度,截断的时候可能少截一位导致数偏小,也有可能多一位导致数变大!
1 2 3 4 5 6 2.45 .toFixed (1 ) 1.45 .toFixed (1 ) 2.45 .toPrecision (20 ) 1.45 .toPrecision (20 )
原型链 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function Person (name ) { this .name = name } const p1 = new Person ('张三' )console .log (p1.__proto__ === Person .prototype ) console .log (Person .prototype .constructor === Person ) console .log (Person .__proto__ === Function .prototype ) console .log (Function .prototype .__proto__ === Object .prototype ) console .log (Object .prototype .__proto__ === null ) console .log (Object .__proto__ === Function .prototype ) console .log (Function .__proto__ === Function .prototype ) console .log (Object .__proto__ === Function .__proto__ )
所有函数的 __proto__
都指向同一个原型对象 Function.prototype
。Object
本身是一个内置的构造函数(用于创建普通对象),作为函数,它的 __proto__
指向 Function.prototype
。Function
也是一个构造函数,Function
是构造所有函数的基类(包括它自身)。Function.__proto__
同样指向 Function.prototype
。
为什么需要原型?JS 语言需要实现面向对象,而原型是面向对象的实现手段之一。一个面向对象的语言必须做到这一点:能判断一个实例的类型。在 JS 中通过原型就可以知道某个对象属于哪个类型,换句话说:原型的存在避免了类型的丢失 。
所有的数组,对象,函数(因为他们都是 w3c 中的 object 类型)都有一个__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.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 ();
面试题:说一下你对原型和原型链的理解 标准答案:js 中每个对象都有其原型对象(隐式原型),通过 __proto__
可以找到该对象的原型对象,原型对象自己本身也有自己的原型对象,从而形成 1 条链条,这就是原型链。原型链的顶端是null
。
加分点 1:和其他语言进行对比。 生成对象一般有两种:
基于类创建对象:java,c++,python
基于已有对象生成对象:Self,Io,js 其实在 js 语言创始时候(1996),基于类创建对象是主流,但是 js 为了实现起来简单(历史原因)选择了第 2 种
加分点 2:从数据结构上进行拓展。 原型链的本质是单链表。之所以没有采用双链表是因为我们大多数场景是需要为子类找到父类,通过父类找子类 1 方面需求几乎没有,2 来实现繁琐性能不高。
创建对象的几种方法
对象字面量
显式使用构造函数
Object.create
1 2 3 const o = {}const oo = Object .create (Object .prototype )
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 8 9 10 11 12 13 14 15 16 17 18 19 function Parent (name ) { this .name = name }Parent .prototype .getName = function ( ) { return this .name }function Child (name ) { Parent .call (this , name) this .age = 10 } Child .prototype = Object .create (Parent .prototype )Child .prototype .constructor = Child const o = new Child ('child' )console .log (o) console .log (o instanceof Parent ) console .log (o instanceof Child ) console .log (o.constructor )
在上面的代码中 Object.create(Parent.prototype)
创建了一个新对象,并将这个新对象的 [[Prototype]]
(即 __proto__
)指向 Parent.prototype
。这样 Child.prototype
可以访问 Parent.prototype
的方法,但修改 Child.prototype
不会影响 Parent.prototype
。执行完上述代码之后 Child.prototype.__proto__ === Parent.prototype
。
基于这个原理,我们可以抽象出继承工具函数:
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 function extend (subClass, superClass ) { const F = function ( ) { }; F.prototype = superClass.prototype ; subClass.prototype = new F (); subClass.prototype .constructor = subClass; subClass.superClass_ = superClass.prototype ; if (superClass.prototype .constructor === Object .prototype .constructor ) { superClass.prototype .constructor = superClass; } } function Person (name ) { this .name = name; } Person .prototype .getName = function ( ) { return this .name ; }; function Author (name, books ) { this .books = books; Author .superClass_ .constructor .call (this , name); } extend (Author , Person );Author .prototype .getBooks = function ( ) { return this .books ; } Author .prototype .getName = function ( ) { const name = Author .superClass_ .getName .call (this ); return name + '->' + this .getBooks ().join (',' ); }; const a = new Author ('村上春树' , ['挪威的森林' , '1Q84' ])const info = a.getName ();
在 node 中可以使用 util.inherits
来简化继承:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function Person (name ) { this .name = name; } Person .prototype .getName = function ( ) { return this .name ; }; function Author (name, books ) { Person .call (this , name); this .books = books; } Author .prototype .getBooks = function ( ) { return this .books ; } require ('util' ).inherits (Author , Person );
nodejs 中实现的原理:
1 2 3 4 5 6 7 8 9 10 11 exports .inherits = function (ctor, superCtor ) { ctor.super_ = superCtor; ctor.prototype = Object .create (superCtor.prototype , { constructor : { value : ctor, enumerable : false , writable : true , configurable : true } }); };
判断属性是否存在
Object.keys
判断自身可枚举属性
Object.prototype.hasOwnProperty
判断自有属性(不扫描原型链)
in
或者 Reflect.has
可以判断自有属性和原型链属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 o = {a :1 } Object .defineProperty (o, 'b' , { enumerable : false , value : 100 }) console .log (Object .keys (o)); o = {a :1 } Object .defineProperty (o, 'b' , { enumerable : false , value : 100 }) Object .prototype .c = 200 console .log (o.a , o.b , o.c ) const existA = o.hasOwnProperty ('a' )const existB = o.hasOwnProperty ('b' )const existC = o.hasOwnProperty ('c' )console .log (existA, existB, existC);
作用域和闭包 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 17 18 function f ( ) { var a = []; for (var i = 0 ; i < 2 ; i++) { var ff = function ( ) { console .log (i) }; a.push (ff); } a.push (function ( ) { i++; }); return a; } const [f1, f2, f3] = f ();f1 (); f3 (); f2 ();
上面会打印什么?答案是 2 和 3。这是因为闭包的另一个机制,同一个变量被引用它的多个闭包所共享 。我们在for
循环内部创建了两个函数,在循环外部创建了一个函数,这三个函数的都引用了f
中的i
,因而i
被这三个函数的闭包所共享,也就是说在i
离开自己所属的作用域时(f
退出前),将只会发生一次拷贝,并将新创建的三个函数的闭包中的i
的对应的指针设定为那一份拷贝的内存地址即可。对于这一个共享的拷贝地址,除了这三个闭包之外,没有其他方式可以访问到它。
必须再次强调的是,被引用的变量拷贝到闭包中的时机发生在、被引用的变量离开自己所属的作用域时,即状态为非活动 时。
考虑下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function f ( ) { const a = []; for (let i = 0 ; i < 2 ; i++) { var ff = function ( ) { console .log (i) }; a.push (ff); } return a; } const [f1, f2] = f ();f1 (); f2 ();
我们知道 ES6 中引入了 let 关键字,由它声明的变量所属块级作用域 。在上面的例子中,我们在 for 循环体的初始化部分使用了 let,这样一来 i 的作用域被设定为了该循环的块级作用域内。不过另一个细节是,循环体中的 i ,也就是 ff 中引用的 i,在每次迭代中都会进行重新绑定,换句话说循环体中的 i 的作用域是每一次的迭代。因此在循环体中,当每次迭代的 i 离开作用域时,它的状态变为非活动的,因此它的内容被拷贝到引用它的闭包中 。
闭包常常会和 IIFE 一起使用,比如:
1 2 3 4 5 6 7 8 9 10 11 12 var a = [];for (var i = 0 ; i < 2 ; i++) { a.push ((function (i ) { return function ( ) { console .log (i) } })(i)); }; const [f1, f2] = a;f1 (); f2 ();
在上面的例子中,让人迷惑的除了闭包的部分之外,就是 i1,i2 和 i3 了。
i1 是 f1 的形参
i2 是 f2 中对外层作用域中的变量的引用
i3 是全局的变量 i,IIFE 执行时 i 对应的值将被作为实参来调用 f1
当 f1 被调用时,也就是 IIFE 执行阶段,它内部创建了一个新的函数 f2,同时也创建了 f2 对应的闭包
由于 f2 中引用了外层作用域中的 i,即 f1 执行期间的 i,且 i 为活动内容,所以 f2 的闭包中添加一条 Key 为 i,Value 为指向 f1 中活动的 i 绑定到的内存单元的地址
当 IIFE 执行完毕,即 f1 要退出的时候,其栈上活动对象 i 就会离开作用域,因此需要将 i 拷贝到引用它的闭包中。
到目前为止,我们看到的例子都引用的直接外层作用域中的变量,那么我们再来看一个例子:
1 2 3 4 5 6 7 8 9 10 11 function f (x ) { return function (y ) { return function (z ) { console .log (x + y + z) } } } const xy = f (1 );const xyz = xy (2 );xyz (3 );
为了方便描述,我们分别标记了 f1,f2,f3。我们在 f3 内部,引用了 x 和 y,并且 x 并不是 f3 的直接外部作用域。那么这个闭包的构建过程时怎样的?
在 JS 中,函数也是以对象的形式存在的,如果将与函数关联的闭包想象成函数对象的一个类型为 Map<string, Value>
的属性也不过分,比如:
1 2 3 4 5 6 7 8 9 const CLOSURE = Symbol ('closure' );const FUN_BODY = Symbol ('fun-body' );const FUN_PARAMS = Symbol ('fun-params' );const funObj = { [FUN_PARAMS ]: [], [FUN_BODY ]: [], [CLOSURE ]: new Map <string, Value >(), }
即使在引擎的实现阶段,因为性能或者实现差异不采用这样的设计,但本质上与这个结构含义是一致的。为了能在运行阶段创建函数对象,在编译阶段就需要收集到必要的信息:
比如在编译 f3 的阶段,我们发现它内部引用了外部的 x 和 y,由于 x 不是直接存在于父及作用域 f2 中的,为了使得未来使用 f2 创建 f3 的时候,仍能够找到 x 的绑定,我们需要将 x 加入到 f2 的闭包中。所以在编译阶段,我们会在 f2 的信息中标注它内部引用了外部变量 x。这样在创建 f2 的时候,x 就会被拷贝到它的闭包中了,等到使用它再创建 f3 的时候,f3 中的 x 也就有了着落。
最后来一个拓展练习:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function f (x ) { return [ function ( ) { x++ }, function (y ) { return function (z ) { console .log (x + y + z) } } ] } const [f1, xy] = f (1 );const xyz = xy (2 );f1 ();xyz (3 );
闭包在实际开发中重要用于封装变量,收敛权限。
什么是闭包,是否会造成内存泄漏 在一个函数的环境中,闭包 = 函数 + 外部的词法环境
1 2 3 4 5 function m ( ){ var a = 1 function sub ( ){} }
广义上来说,上面的闭包定义是没有问题的。但是我们经常说的闭包是狭义上的,也就是下面的:
1 2 3 4 5 6 7 8 9 function m ( ){ var a = 1 function sub ( ){ a; } return sub () } const s = m ()
内存泄漏分为 2 种情况:
持有了本该被销毁的函数,造成其词法环境无法被销毁
当有多个函数共享词法环境的时候可能导致词法环境膨胀,从而导致:无法访问也无法销毁的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 function createXXX ( ){ const bigData = 'x' .repeat (1000000 ) const smallData = 'x' function big ( ){ bigData; } function small ( ){ smallData; } return small; } const xxx = createXXX ()
闭包和提权漏洞 首先看一道面试题:如何在不改变下面代码的情况下修改 obj
对象?
1 2 3 4 5 6 7 8 9 10 11 var o = ((function ( ) { var obj = { a : 1 , b : 2 } return { get (k ) { return obj[k] } } }))()
obj 是在 o 函数的内部定义的,并且内部通过访问器对外部提供访问。我们可以在 Object
的原型上添加方法,返回自己本身!
1 2 3 4 5 6 7 8 9 10 11 12 Object .defineProperty (Object .prototype , 'hack' , { get ( ) { return this } }) const r = o.get ('hack' )console .log ('拿到 obj 对象:' , r);r.a = 'hacked' r.hello = 'c' delete r.b console .log (o.get ('a' ), o.get ('b' ), o.get ('hello' ));
Node 服务器如果出现上述情况将会非常严重!可能导致一个恶意的第三方库搞花活篡改了另一个第三方库的代码,导致服务器崩溃!防御手段就是将原型置空!
1 2 3 4 5 6 7 8 9 10 11 12 13 var o = ((function ( ) { var obj = { a : 1 , b : 2 } Object .setPrototypeOf (obj, null ) return { get (k ) { return obj[k] } } }))()
这也就是为什么一些第三方库使用 Object.create(null)
来创建一个空对象,防止被篡改。
实现 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 ()
注意 myNew
函数的第 5 行,是为了兼容以下这种情况
1 2 3 4 5 function Person (name, age ) { this .name = name; this .age = age; return { name : '张三' , age : 18 } }
实现 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 函数 区别于 call 和 apply,bind 不会立即执行函数,而是返回一个绑定了 this 的新函数。bind 可以预先传递部分参数(柯里化)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 )); })();
每次调用 overload 时,新函数会捕获 oldMethod(即之前的函数),形成闭包。多次调用 overload 添加不同的函数时,会形成一个链式结构,逐层检查参数个数。
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 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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); }); } 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进程。
事件循环 参考国外大神写的一系列文章
1 2 3 4 function foo ( ) { setTimeout (foo, 0 ) } foo ()
上面的代码为什么不会导致栈溢出?
因为我们是把函数放在一个异步 的环境中了,foo 将一个函数通知给了计时线程后就不管了,foo 函数就执行完了,其执行栈就被清空了!计时线程等时间到了之后将 foo 加入事件队列,取出来执行,执行的过程和第一次执行一模一样!
怎么实现一个精准的 setInterval 首先要明白为什么不精准?
事件循环影响回调的执行时机
嵌套 5 层以上有最小 4ms 的限制
失活页面间隔会被强制调整到 1s
解决方案:
每次回调的时候重新调整时间偏差,Date.now, perfermance.now
requestAnimationFrame 不受页面失活影响,但是其他因素影响(os 本身的忙碌程度,正在玩游戏)
web worker:运行在后台线程中,不受主线程事件循环的影响,但是封装费事
CommonJS 和 ESM 的区别是什么 从标准来源上说:CJS 是 node 社区标准,定义了一个 require 函数,一个全局可用的 module 对象,上面有一个 exports 属性;ESM 是官方标准,是新增的语法。 从时态上面来说:CJS 是运行时确定依赖关系,ESM 同时支持编译时(静态)和运行时(import()
语法)。
因此 import a from 变量
这种写法在 ESM 中会直接语法报错,在函数中使用 import
关键字也是错误的!
静态导入带来了非常多的好处,最显著的就是不用等到运行时就能确定依赖关系。前端技术中常见的树摇优化(Tree Sharking)就使用了静态分析依赖关系从而移除不必要的导入来减少了代码体积。
一些场景设计题 参考资料