JS探秘——那些你理解存在偏差的问题

Javascript的连续赋值运算

var a = {n:1};  
a.x = a = {n:2};  
alert(a.x); // --> undefined 

看 jQuery 源码 时发现的这种写法。 以上第二句 a.x = a = {n:2} 是一个连续赋值表达式。 这个连续赋值表达式在引擎内部究竟发生了什么?是如何解释的?

加一个变量 b,指向 a。

var a = {n:1};  
var b = a; // 持有a,以回查  
a.x = a = {n:2};  
alert(a.x);// --> undefined  
alert(b.x);// --> [object Object] 

实际执行过程:从右到左,a 先被赋值为 {n:2},随后 a.x 被赋值 {n:2}。第一步 a = {n:2} 的 a 指向的是新的对象 {n:2}, 第二步 a.x = {n:2} 中的 a 是 {a:1}。如下所示

             a.x  =  a  = {n:2}
              │      │
      {n:1}<──┘      └─>{n:2}

为什么 ++[[]][+[]]+[+[]] = 10?

++[[]][+[]]+[+[]],它相等于:

++[[]][+[]]
+
[+[]]

在 JavaScript 里,+ 会把一些字符转化成数字,如 +"" === 0、+[] === 0,因此,我们可以简化一下(++ 比 + 有更高的优先级):

++[[]][0]
+
[0]

因为 [[]][0] 的意思是:获取 [[]] 的第一个元素,这就得出了下面的结果:

  • [[]][0] 返回内部数组 ([])。根据语言规范,我们说 [[]][0] === [] 是不正确的,但让我们把这个内部数组称作 A,以避免错误的写法。
  • ++[[]][0] == A + 1, 因为 ++ 的意思是”加一”。
  • ++[[]][0] === +(A + 1);换句话说,你得到的永远是个数值( +1 并不一定得到的是个数值,但 ++ 一定是)。

我们可以把这一堆代码简化的更清晰。让我们把 A 换回成 [] :

+([] + 1)
+
[0]

在 JavaScript 里,这也是正确的:[] + 1 === "1",因为 [] == "" (这相当于一个空的数组的内部元素连接),于是:

+([] + 1) === +("” + 1),并且 
+("” + 1) === +("1"),并且 
+("1") === 1 

让我们再次简化一下:

1
+
[0]

同样,在 Javascript 里,这是正确的:[0] == "0",因为这是相当于一个有一个元素的数组的内部元素的连接。 各元素会使用,分隔。 当只有一个元素时,你可以推论出这个过程的结果就是它自身的第一个元素。

所以,最终我们得到 (数字 + 字符串 = 字符串):

1
+
"0"
=== "10" // 耶!

[1,2] + [3,4]=="1,23,4"

JavaScript 的 + 运算符有两个目的:

  1. 将两个数相加
  2. 将两个字符串连接

规范并没有定义 + 运算符在数组上的行为,所以javascript 首先 把数组转换成字符串,然后在字符串上进行 + 运算。

如果想连接两个数组,可以使用数组的 concat 方法:

[1, 2].concat([3, 4]) // [1, 2, 3, 4]

{}+{}等于多少?

结果为:NaN

这个问题的原因是,JavaScript 把第一个 {} 解释成了一个空的代码块(code block)并忽略了它。 NaN 其实是表达式 +{} 计算的结果 (+ 加号以及第二个 {})。 你在这里看到的 + 加号并不是二元运算符「加法」,而是一个一元运算符,作用是将它后面的操作数转换成数字,和 Number() 函数完全一样。例如:

> +"3.65"
3.65

以下的表达式是它的等价形式:

+{}
Number({})
Number({}.toString())  // {}.valueOf() isn’t primitive
Number("[object Object]")
NaN

为什么第一个 {} 会被解析成代码块(code block)呢? 因为整个输入被解析成了一个语句:如果左大括号出现在一条语句的开头,则这个左大括号会被解析成一个代码块的开始。 所以,你也可以通过强制把输入解析成一个表达式来修复这样的计算结果: (译注:我们期待它是个表达式,结果却被解析成了语句)

> ({} + {})
'[object Object][object Object]'

{} === {},结果为false

每个对象都有自己唯一的标识符,因此通过字面量或构造函数创建的对象和任何其他对象都不相等,我们可以通过 === 进行比较。

{} === {} //false

对象是通过引用来比较的,只有两个对象有相同的标识,才认为这个对象是相等的。

var obj = {};
 obj === obj //true

原始值没有内部标识,原始值是按值比较的: 比较两个原始值的依据是他们的内容,如果两个原始值的内容相同,这认为这两个原始值相同。

"abc" === "abc" //true

原始值和它们的包装类型

原始值类型 boolean, number 以及 string 都有自己对应的包装类型 Boolean, Number 和 String。 包装类型的实例都是对象值,两种类型之间的转换也很简单:

  • 转换为包装类型:new String("abc")
  • 转换为原始类型:new String("abc").valueOf()

原始值类型以及它们相应的包装器类型有很多不同点,例如:

 typeof "abc" //'string'
 typeof new String("abc") //'object'
 "abc" instanceof String //false
 new String("abc") instanceof String //true
 "abc" === new String("abc") //false

包装类型的实例是一个对象,因此和 JavaScript 和对象一样,包装类型也无法进行值的比较(只能比较引用)。

 var a = new String("abc");
 var b = new String("abc");
 a == b //false(虽然 a 和 b 有相同的内容,但是依然返回 false)
 a == a //true

5.toFixed(3),报错SyntaxError: Unexpected token ILLEGAL

原始值没有自己的方法

包装对象类型很少被直接使用,但它们的原型对象定义了许多其对应的原始值也可以调用的方法。 例如,String.prototype 是包装类型 String 的原型对象。 它的所有方法都可以使用在字符串原始值上。 包装类型的方法 String.prototype.indexOf 在字符串原始值上也有,它们并不是两个拥有相同名称的方法,而的的确确就是同一个方法:

 "abc".charAt === String.prototype.charAt
true

在数字的包装类型 Number 的原型对象有 toFixed 方法,即 Number.prototype.toFixed,但是当我们写如下代码时却发生错误:

 5.toFixed(3)
SyntaxError: Unexpected token ILLEGAL

此错误是解析错误(SyntaxError),5 后面跟着一个点号(.),这个点被当作了小数点,而小数点后面应该是一个数,以下代码可以正常运行:

 (5).toFixed(3)
"5.000"
 5..toFixed(3)
"5.000"
posted @ 2017-11-09 18:06  MasterYao  阅读(661)  评论(0编辑  收藏  举报