I don’t know about JS
一、Additive & Unary
js中+
,-
,~
都会把对象(甚至function)转换为表达式
例如
+function(){} // NaN
-function(){} // NaN
~function(){} // -1
+[] // 0
-[] // 0
~[] // -1
+{} // NaN
-{} // NaN
~{} // -1
But!你以为你就懂JS了吗?too young too simple,看看下面这些,猜猜结果是什么?
- [] + []
- {} + []
- [] + {}
- {} + {}
- {} + {};
结 |
果 |
如 |
下 |
↓ (Chrome和Node的运行结果)
- [] + [] // “”
- {} + [] // 0
- {} + []; // [object object]
- [] + {} // [object object]
- {} + {} // [object object][object object]
- {} + {}; // NaN
神不神奇,就问你神不神奇!
================= 华丽的分割线 =================
首先,在ECMA中,+
这个符号,有2种操作(赋值类的+=
可以涵盖在additive中):
- unary
- additive
unary的含义是一元操作符,顾名思义,处理成unary是因为JS的解释器认为目前只有一个操作对象,如果认为有2个对象,则会处理成additive。
那么unary和additive有什么区别呢?
+
的unary很简单,直接toNumber,关于ToNumber对JS原生对象的映射关系如下(ECMA2020)
Argument Type | Result |
---|---|
Undefined | Return NaN |
Null | Return +0𝔽 |
Boolean | If argument is true, return 1𝔽 If argument is false, return +0𝔽 |
Number | Return argument (no conversion) |
String | See grammar and conversion algorithm below |
Symbol | Throw a TypeError exception |
BigInt | Throw a TypeError exception |
Object | Apply the following steps: 1. Let primValue be ? ToPrimitive(argument, number) 2. Return ? ToNumber(primValue) |
Object的转换稍微有点复杂,关于ToPrimitive,下面会介绍。
下面我们看additive operation,相比unary,这个操作就复杂一些了。ECMA2020中关于additive operation这样说:
The addition operator either performs string concatenation or numeric addition.
其实意思就是判断应该做number还是string的转换,然后相加或者拼接。
additive具体的步骤是:
If opText is
+
, then
- Let lprim be ? ToPrimitive(lval).
- Let rprim be ? ToPrimitive(rval).
- If Type(lprim) is String or Type (rprim) is String, then
- Let lstr be ? ToString(lprim).
- Let rstr be ? ToString(rprim).
- Return the string-concatenation of lstr and rstr.
- Set lval to lprim.
- Set rval to rprim.
以及容易被忽略的一个NOTE
No hint is provided in the calls to ToPrimitive in steps 1.a and 1.b. All standard objects except Date objects handle the absence of a hint as if number were given; Date objects handle the absence of a hint as if string were given. Exotic objects may handle the absence of a hint in some other manner.
因此,当出现+(additive operation),就会对两个值进行ToPrimitive转换,转换完之后如果发现任意一个值是string,就全部转换为string再拼接,否则作为数值相加。Note说对于Date对象给hint是string,而其他object,如果hint没有指定就默认number。由于[]和{}都是object,所以ToPrimitive的hint是number
那么toPromitive又是什么rule:
- Assert: input is an ECMAScript language value.
- If Type(input) is Object, then
- Let exoticToPrim be ? GetMethod(input, @@toPrimitive).
- If exoticToPrim is not
undefined
, then- If preferredType is not present, let preferredType be number.
- Return ? OrdinaryToPrimitive(input, preferredType).
- Return input.
这里纠结了很久,想半天,按照exoticToPrim来算,就不应该是toString的结果(例如[]+[]是”“),而且@@toPrimitive找半天没找到,又在想是不是在additive之前有一个GetValue导致[]已经转换成”“了,结果看了半天GetValue和@@toPrimitive。事实上GetValue还是返回原值(对于Reference的[]就是自身),而@@toPrimitive对Array就不存在,从而exoticToPrim就是undefined(我摔!),exoticToPrim和@@toPrimitive都是针对Date对象的特殊处理。总而言之,言而总之,对于[]或者{}来说,它们都是走OrdinaryToPrimitive的method,下面是这个func定义
- Assert: Type(O) is Object.
- Assert: hint is either string or number.
- If hint is string, then
- Let methodNames be « “toString”, “valueOf” ».
- Else,
- Let methodNames be « “valueOf”, “toString” ».
- For each element name of methodNames, do
- Let method be ? Get(O, name).
- If IsCallable(method) is true, then
- Let result be ? Call(method, O).
- If Type(result) is not Object, return result.
- Throw a TypeError exception.
瞬间就明朗了,就算hint是number,先取valueOf,得到[]自身,并不是primitive value,因此再一次toString了。因此[]转换成’‘,{}转换成’[object object]’。同时,如果你注意unary的Object处理rule,是先toPrimitive,然后再toNumber,所以对[],相当于toNumber(‘’);对{},相当于toNumber(‘[object object]’),从而得到0和NaN,如此就很make sense
二、 Comparison Operation(Relational/Equality)
还有一些神奇的表达式操作:
NaN <= 0
NaN == 0
NaN < 0
NaN == NaN
// above are all false, as expected, but let's see below
null <= 0 // true
null >= 0 // true
null == 0 // false
null < 0 // false
null == null // true
1 + null // 1
typeof null // "object"
历史遗留问题:判断object是二进制前4位是不是0,然而null全部是0
对于null,是一种单独的Type,所以对于Type(x)或者ToPrimitive(x),null就是原值,并不会转换
那么继续研究ECMA:
- 对Abstract Relational Comparison
- If the LeftFirst flag is true, then
- Let px be ? ToPrimitive(x, number).
- Let py be ? ToPrimitive(y, number).
- Else,
- NOTE: The order of evaluation needs to be reversed to preserve left to right evaluation.
- Let py be ? ToPrimitive(y, number).
- Let px be ? ToPrimitive(x, number).
- If Type (px) is String and Type (py) is String, then
- If IsStringPrefix(py, px) is true, return false.
- If IsStringPrefix(px, py) is true, return true.
- Let k be the smallest non-negative integer such that the code unit at index k within px is different from the code unit at index k within py. (There must be such a k, for neither String is a prefix of the other.)
- Let m be the integer that is the numeric value of the code unit at index k within px.
- Let n be the integer that is the numeric value of the code unit at index k within py.
- If m < n, return true. Otherwise, return false.
- Else,
- If Type (px) is BigInt and Type (py) is String, then
- Let ny be ! StringToBigInt(py).
- If ny is NaN, return undefined.
- Return BigInt::lessThan(px, ny).
- If Type (px) is String and Type (py) is BigInt, then
- Let nx be ! StringToBigInt(px).
- If nx is NaN, return undefined.
- Return BigInt::lessThan(nx, py).
- NOTE: Because px and py are primitive values, evaluation order is not important.
- Let nx be ! ToNumeric(px).
- Let ny be ! ToNumeric(py).
- If Type(nx) is the same as Type(ny), return Type(nx)::lessThan(nx, ny).
- Assert: Type(nx) is BigInt and Type(ny) is Number, or Type(nx) is Number and Type(ny) is BigInt.
- If nx or ny is NaN, return undefined.
- If nx is -∞𝔽 or ny is +∞𝔽, return true.
- If nx is +∞𝔽 or ny is -∞𝔽, return false.
- If ℝ(nx) < ℝ(ny), return true; otherwise return false.
前面对于null都不满足,于是走到了第4步else,4.4 nx be !ToNumeric(px),得到0,于是,0 <= 0是true,但是0 < 0是false,这就是为什么null <= 0但是不null < 0了。
那么为什么null == 0是false呢,因为==和<=在ECMA中走的flow不同,==执行Abstract Equality Comparison,其rule对应如下
- 对Abstract Equality Comparison
- If Type(x) is the same as Type(y), then
- Return the result of performing Strict Equality Comparison x === y.
- If x is null and y is undefined, return true.
- If x is undefined and y is null, return true.
- NOTE: This step is replaced in section B.3.7.2.
- If Type(x) is Number and Type(y) is String, return the result of the comparison x == ! ToNumber(y).
- If Type(x) is String and Type(y) is Number, return the result of the comparison ! ToNumber(x) == y.
- If Type (x) is BigInt and Type (y) is String, then
- Let n be ! StringToBigInt(y).
- If n is NaN, return false.
- Return the result of the comparison x == n.
- If Type(x) is String and Type(y) is BigInt, return the result of the comparison y == x.
- If Type(x) is Boolean, return the result of the comparison ! ToNumber(x) == y.
- If Type(y) is Boolean, return the result of the comparison x == ! ToNumber(y).
- If Type(x) is either String, Number, BigInt, or Symbol and Type(y) is Object, return the result of the comparison x == ? ToPrimitive(y).
- If Type(x) is Object and Type(y) is either String, Number, BigInt, or Symbol, return the result of the comparison ? ToPrimitive(x) == y.
- If Type (x) is BigInt and Type (y) is Number, or if Type (x) is Number and Type (y) is BigInt, then
- Return false.
可以看到,对于null == 0
的比较,没有满足的条件可以找到,所以到14返回false。
类似的,这里还有一个tricky的情况,就是
[] == true // false
[] == '' // true
!![] // true
因为==操作对[]采取第12条,所以ToPrimitive(x)就变成了’‘,从而不等于true,然而当直接判断!![]或者if([])这样的操作时,采用的是ToBoolean,其rule对应如下
Argument Type | Result |
---|---|
Undefined | false |
Null | false |
Boolean | return argument |
Number | if argument is +0𝔽, -0𝔽, NaN, return false; otherwise return true |
String | if argument is the empty String, return false; otherwise return true |
Symbol | true |
BigInt | if argument is 0, return false; otherwise return true |
Object | true |
于是乎!![]就变成了true,因为它是object
三、词法作用域
function bar() {
console.log(myName)
}
function foo() {
var myName = '老袁';
bar();
}
var myName = '京城一灯';
foo();
对于bar里面的myName,只会向上查找变量,因此myName有变量提升,相当于在bar上面有一个
var myName;
而并不会查找foo里面的myName,那不在bar函数的上方,因此如果倒数第二行的myName被注释掉会直接报错,从而结果也理所应当的是京城一灯
四、GC
- 浏览器什么时候会GC,即便是给引用赋值为null,也不会立马执行GC,对象仍然存在于内存空间中
- 而且使用闭包应当注意,因为一旦使用闭包,就会造成类似的问题,闭包的数据无法被回收,造成内存泄漏
- 对于eval()函数更甚,使用eval的地方就会形成闭包,因为eval里万一有需要使用的变量,宿主环境是无法负责的,因此只能给你闭包起来保证变量在这,所以使用eval要小心
- 即便你要使用eval,使用window.eval()可以避免内存泄漏问题,因为这个命令就是让eval到全局window去查找需要的变量
with
,遇到with里的变量,放弃所有GC,并且将变量丢到全局- try catch中的catch(e),e也会造成内存泄漏
五、Function
下面这段代码
var a = 'outside'
function init() {
var a = 'inside';
var fn1 = new Function(console.log(a));
var fn2 = new Function('console.log(a)');
}
fn1(); // inside
fn2(); // outside
因为Function在遇到string的时候,是在全局作用域查找变量的
六、JS真的万物皆对象吗?
typeof String // function
JavaScript中有一些内部未暴露的对象,并不是Object
对于下面的原型链对象
Object.prototype.a = 'o';
Function.prototype.a = 'f';
var Person = function(){}
console.log(Person.a) // 'f'
console.log(new Person().a) // 'o'
1..a // 'o'
new出来的对象o1,o1的原型链是Person,Person的原型链是Object,从而找到了a为o
,
而直接使用Person,其原型链是Function,所以找到了f
,另外Function再网上找的话,原型链也是Object
1..a结果是o
是因为1.是合法的,JS里的Number都是浮点数,所以1.0和1.是一样的
七、Meta Programming
类似修改valueOf,toString的一些底层方法,从而增强当前对象的功能
var obj = {'a':2, 'b':3};
Object.defineProperty(obj, Symbol.iterator, {
value: function() {
var o = this;
var idx = 0;
var ks = Object.keys(o);
return {
next: function() {
return {
value: o[kx[idx]],
done: idx > ks.length,
}
}
}
}
})
for(let v of obj) {
console.log(v); // done
}
通过元编程就可以解决这个问题
另外,甚至可以改变toPrimitive等Symbol的Func,如
var yideng = {
[Symbol.toPrimitive]: ((i) => () => ++i;)(0),
}
if((yideng == 1) & (yideng == 2) & (yideng == 3)) {
console.log('success here') // done
}
还有几点跟元编程相关
- TCO尾递归优化,可以通过TCO_ENABLE=true打开(hosting支持的话)
- Reflect也是基于元编程实现,例如npm库reflect-metadata,做IOC会用到
八、协程
- async,await是microtask,不是macrotask,但是会保存变量,类似闭包,保存await的环境
- await就是Promise.then
- JS是单线程执行,但是V8有多个线程辅助,
九、JS实现多线程
- Concurrent.Thread.js (Web Worker不灵的时候,可以临时替代)
- web worker
- 现在基本都用原子锁了