重新学习JS类型和语法

103 阅读22分钟

重新学习JS类型和语法

如果valueOf返回非法值,那么也不会检查toString了?

类型

内置类型

众所周知,JS有其中内置类型:

  • 空值null
  • 未定义undefined
  • 布尔值boolean
  • 数字number
  • 字符串string
  • 对象object
  • 符号symbol

其中,我们在检查类型的时候通常使用typeof操作符来检查

但是,null在使用typeof操作符获取类型的时候,得到的却是object,所以我们需要用复合类型来检测null类型的值:

 var a = null
 (!a && typeof a === 'object')   //true

检查函数是使用typeof操作符,会返回function,看起来像函数是JS的一个类值类型,但实际上他是Object的一个子类型,函数是可调用对象,因为其有内部属性[[Call]]

函数中的属性length表示的是声明的参数个数

而数组的属性length表示的是元素的个数

值和类型

这一节着重讨论一下undefinedundeclared两者的关系

undefiend:已经在作用域中声明过,但是并未赋值

undeclared:没有在作用域中声明过

这两者在浏览器中的处理也是不同的,出乎意料的:

这里的b未被声明,而报错容易让人误会是没赋值undefined

 var a 
 a   //undefined
 b   //ReferenceError: b is not defined

并且,typeof操作符对两者的处理也是出乎意料的:

对两者的处理返回都是undefined,对于未声明的变量返回也是undefined

这是因为**typeof有一个特殊的安全防范机制**

 var a 
 typeof a    //'undefined'
 typeof b    //'undefined'

typeof的安全防范机制:

当我们在其他文件引入变量的时候,为了防止引入的变量不存在,导致我们的程序报错,我们通常可以使用typeof来防范

 //如果引入的a不存在则会报错
 if(a){
  console.log(a) 
 }
 //安全
 if(typeof a !== undefined){
  console.log(a)
 }

除此之外,我们也可以用全局变量的检查来代替typeof安全防范机制,但是通常不会这么做,因为如果代码运行在多个JS环境中,全局对象并非就是window了(如Node)

这一章着重介绍JS的集合内置值类型:arraystringnumber

数组和字符串

数组

数组的元素可以容纳所有类型的值,多维数组就是通过这种方式来实现的

通过**delete运算符可以将元素从数组中删除**,但是不会改变长度length

创建稀疏数组(即含有空白或空缺单元的数组),空白单元值为undefined

数组也是一个对象,所以也可以添加字符串键值和属性,但是不计算在length长度内,如果字符串键值能够被强制转化为十进制数据的话,这个属性就会被当作数字索引来处理

 var a = []
 a["13"] = 22
 a.length  //14

字符串

字符串经常可以看成字符数组,都有length属性和indexOf()concat()

但是在JS中,字符串是不可变的,而数组是可变的,所以我们通过a[1]这种方法直接修改字符串是不可行的,所以我们可以将其转化为数组处理,再转化为字符串

获取字符串的某一位字符,可以使用属性直接读取a[1],也可以通过**a.charAt(1)** 来读取(不传参默认为0

数字

基础语法

JS中,数字字面量一般用十进制表示,下述代码表示的都是42:

 var a = 42
 var a = .42
 var a = 42.

默认情况下,数字都以十进制显示小数部分最后面的0被省略

特别大和特别小的数字用指数格式显示,与toExponential()函数输出结果一样

tofixed方法输出的实际上是给定数字的字符串形式,可以指定显示的小数部分的位数

 var a = 42.16
 a.toFixed(1)    //'42.2'

而**toPrecision方法则可以指定有效数位的显示位数**:

 var a = 42.16
 a.toPrecision(1)    //'4e+1'

需要注意.运算符,因为他是一个有效的数字字符会被优先识别为数字字面量的一部分,然后才是对象属性访问运算符

 42.tofixed(3)   //SyntaxError
 //这里会把42.看成一个整体,导致没有.操作符调用tofixed

ES6其他进制的表示方式:0x0b0o,分别代表十六进制、二进制、八进制

较小的数值

我们常常可以看到一个面试题:

 0.1 + 0.2 === 0.3   //false

这是因为二进制浮点数中的0.10.2,并不是十分准确,相加的结果并非刚好等于0.3,而是一个比较接近的数字0.30000000000000004,所以判断结果为false,这种情况在很多语言都存在

解决这个问题的方法是设置一个误差范围值,也就是机器精度,在JS中,这个值通常为2-52,在ES6中,这个值被定义在**Number.EPSILON**中,可以直接使用,所以现在判断两个数字是否相等可以这样实现:

 function numbersCloseEnoughToEqual(n1, n2){
     return Math.abs(n1 - n2) < Number.EPSILON
 }

整数范围

最大整数:Number.MAX_VALUE,约为1.798e+308

最小整数:Number.MIN_VALUE,约为5e-324,不是负数,但是无限逼近于0

最大安全整数:Number.MAX_SAFE_INTEGER,即253

最小安全整数:Number.MIN_SAFE_INTEGER,即-253

整数检测

检测一个数是否为整数,可以使用:Number.isInteger()

 Number.isInteger(42.00) //true

检测一个数是否为安全的整数,可以使用:Number.isSafeInteger

 Number.isSafeInteger(Math.pow(2, 53))   //false

特殊数值

不是值的值

undefined类型的值只有一个值,即undefined

null类型的值也只有一个值,即null

null:指空值,即曾经赋过值,当时目前没有值,是一个特殊关键字,不是标识符,不能当作变量使用

undefined:指没有值,即从未赋过值,是一个标识符,可以当作变量使用

undefined

由于**undefined是一个标识符**,所以我们可以为其赋值(非严格模式下)

 undefined = 1
 undefined   //undefined

但是修改并不会成功,使用Object.getOwnPropertyDescriptor获取window上的undefined就可以看到他的属性描述:writable:false,不可修改,所以在非严格模式下会静默失败,如果是严格模式则会报错

虽然直接修改全局上面的undefined不可取,但是我们可以在函数作用域中声明一个undefined的局部变量

 function foo(){
     var undefined = 2
     console.log(undefined);
 }
 foo()   // 2

总而言之,虽然undefined可以被自定义,但是我们不要去定义undefined

前面说过,undefined是一个内置标识符,它的值是undefined,那我们怎么得到这个值呢?

可以使用void运算符得到undefined表达式void xxx表示没有返回值,所以返回结果则是undefinedvoid不改变表达式的值,只是让表达式不返回值

 var a = 22
 console.log(void a, a)  //undefined 22

这个运算符的用处通常在于不让表达式返回值,下面两种方法效果一样

 if(!APP.ready){
     return void setTimeout(dosomething, 100)
 }
 if(!APP.ready){
     setTimeout(dosomething, 100)
     return
 }

特殊的数字

这里一共介绍三个特殊数字:NaN、无穷值和零值

  • NaN执行数学运算没有成功,失败后无法返回有效的数字,则返回NaN

    NaN唯一一个非自反的值NaN === NaNfalse

    所以判断NaN的方式比较特殊,我们可以使用全局工具函数isNaN()来判断一个值是否是NaN

     var a = 2 / 'foo'
     isNaN(a)    //true
    

    但是**isNaN有一个缺陷**,就是检查非数字类型字符串时会返回ture,我们来看看isNaN的实现:

     var isNaN = function(value) {
         var n = Number(value)
         return n !== n
     }
    

    由于使用了Number函数,传入字符串之后,n就被赋值为NaN,所以isNaN也就返回true

    为了解决这个缺陷,ES6提供了**Number.isNaN()**

     var a = 'foo'
     Number.isNaN(a) //false
    
  • 无穷数:如果数学运算得到比最大整数大的数,则返回Infinity

     var a = 1 / 0   //Infinity
     var b = -1 / 0  //-Infinity
    

    如果**Infinity除以Infinity,则会得到NaN**,因为JS语言没有定义这个操作

    如果有穷正数除以Infinity,则得到0

    如果有穷负数除以Infinity,则得到-0

  • 零值:

    JS的零值有常规0(也叫做+0 和**-0**

    对负零字符串化,会得到'0'

     var a = 0 / -3
     a.toString()    //'0'
     a + ''  //'0'
     JSON.stringify(a)   //'0'
    

    将负零的字符串转化为数字,得到的结果则为-0

     + '-0'  //-0
     Number('-0')    //-0
     JSON.parse('-0')    //-0
    

    负零的一些操作也是比较疑惑的:

     0 === -0    //true
    

    所以我们要区分0-0,就需要做一些特殊处理

     function isNegZero(n){
         n = Number(n)
         return (n === 0) && (1 / n === -Infinity)
     }
     isNegZero(-0)   //true
    

特殊等式

在上述的特殊数值中,NaN-0在相等比较上比较特别,所以ES6为我们提供了一个方法来判断两个值是否绝对相等:Object.is()

 var a = 2 / 'foo'
 var b = -3 * 0
 Object.is(a, NaN)   //true
 Object.is(b, -0)    //true
 Object.is(b, 0)     //false

原生函数

内部属性[[Class]]

所有的typeof返回值为object的对象,都包含一个内部属性[[Class]],可以看成是一个内部的分类

这个属性一般无法直接访问,而是通过**Object.prototype.toString**来查看

 Object.prototype.toString.call([1, 2, 3])   //[Object Array]
 Object.prototype.toString.call(null)    //[Object Null]

如果传入的是非undefinednull的基本类型值,如字符串等,情况会有所不同,会被各自的封装对象自动包装

 Object.prototype.toString.call('abc')   //[Object String]
 Object.prototype.toString.call(22)  //[Object Number]

封装对象包装

如果基本类型要访问像length这些属性或方法,就需要通过封装对象才能访问,此时JS会自动为基本类型值包装一个封装对象

我们不需要自己一开始就创建一个封装对象,因为浏览器已经对这种情况做了优化,如果一开始就创建封装对象的话,反而会降低执行效率

如果用封装对象的话,使用if判断时,会得到true,因为永远为对象

拆封

如果想要得到封装对象中的基本类型值,可以使用**valueOf()** 函数

 var a = new String('abc')
 a.valueOf() //'abc'

但是大部分情况下都不用我们自己显式的去拆封,强制类型转换会为我们隐式拆封

 var a = new String('abc')
 var b = a + ''  //'abc' string类型

原生函数作为构造函数

Array

 var a = new Array(1, 2, 3)
 a   //[1, 2, 3]
 var b = [1, 2 ,3]
 b   //[1, 2, 3]

构造函数Array不要求一定要带上new关键字,不带的时候会自动补上

Array只带一个数字参数的时候,该参数会被作为数组的预设长度而不是充当数组的一个元素,在浏览器中会显示 [empty × n]

创建空单元的数组方法有:

  • 用**new Array创建**,参数传入一个数字,代表空单元的数量
  • 创建一个空数组,然后直接修改长度length

创建包含undefined的数组方法有:

  • 直接字面量指定数组元素为undefined

  • 通过apply创建**Array.apply(null, {length: 3})**

    可以看成apply中有一个for循环,从0循环到lengthfor循环就会去查找参数数组arr[0]、arr[1]等,但是{length: 3}中并不存在这些属性,所以返回值为undefined

    实际上执行的就是Array(undefined, undefined, undefined)

这两种方式创建出来的数组行为上有时相同,有时不同:

 var a = new Array(3)
 var b = [undefined, undefined, undefined]
 a.join('-') //--
 b.join('-') //--
 a.map(function(v, i){return i}) //[undefined * 3]
 b.map(function(v, i){return i}) //[0, 1, 2]

Object、Function、RegExp

  • Object

    一般不用这种方式来创建一个对象,因为这样就不能一次性设置多个属性

  • Function

    一般也不用这种方式创建函数,除非要动态定义函数参数和函数体

  • RegExp

    使用常量来定义正则表达式,不仅语法简单,执行效率也高,因为JS引擎在代码执行之前会对它进行预编译和缓存

    但是动态进行正则表达式式就必须使用构造函数形式

Date和Error

  • Date

    创建日期对象必须使用**new Date**,直接使用Date则会返回日期的字符串值,而不是对象

    Date可以带参数,用来指定日期和时间不带参数则使用当前日期和时间,用这种方式创建会返回一定的日期格式:'Sun Aug 07 2022 12:13:11 GMT+0800 (中国标准时间)'

    如果要获取时间戳,则可以通过日期对象中的**getTime**方法来获得

     (new Date()).getTime()  //1659845742457
    

    ES5之后引入了一个更简单的方法:Date.now(),获取当前时间戳

  • Error

    该构造函数也是带不带new都可以

    创建错误对象主要是为了获得当前运行栈的上下文栈的上下文信息包括函数调用栈信息和产生错误的错误代码行号,以便于调试

强制类型转化

将值从一种类型转化到另一种类型,这是显式的情况,隐式的情况叫做强制类型转化

类型转化发生在静态类型语言的编译阶段,强制类型转化发生在动态类型语言的运行时

抽象值操作

首先,我们先了解一下字符串、数字、和布尔值之间的类型转化基本规则(以下的只是规则,并不是具体方法)

  • ToString

    负责处理非字符串到字符串之间的强制类型转化

    基本类型值的字符串规则:null转化为'null'undefined转化为'undefined'true转化为'true',数字的转化遵循通用规则,极大和极小的数字使用指数形式

    对于普通对象来说,除非自己定义,否则toString返回内部属性[[Class]] ,如[object Object]

    数组的默认toString方法重新定义过了,将所有单元字符串化之后再用','连接起来再返回

  • ToNumber

    负责处理非数字值到数字值之间的强制类型转化

    对象会被先转化为相应的基本类型值,如果返回的是非数字的基本类型值,则在遵循以上规则强制转化为数字

    为了将值转化为相应的基本类型值,抽象操作ToPromitive先检查该值是否有valueOf方法,如果有并且返回基本类型值,就将其值进行强制类型转化,如果没有就使用toString方法的返回值,如果两者都不返回基本类型值,则报错

    如果valueOf返回非法值,那么也不会检查toString

  • ToBoolean

    JS中的值可以分为以下两类:

    • 可以被强制类型转化为false的值
    • 其他(被强制类型转化为true的值)

    假值有:undefinednullfalse+0-0NaN'',这些值被强制类型转化都为false

    处理这些以外的值被强制类型转化为布尔值则为true

显式强制类型转化

这一节介绍以下显式强制类型转化,各种数据类型之间的强制转化

字符串和数字之间的显式转化

字符串和数字之间的转换是通过String()Number()这两个内建函数的前面没有new关键字,并不会创建一个封装对象,这两个方法都是遵循上面的我们讲过的规则

除了这两种方法之外,还有其他方法可以实现,例如**toString()** ,不过这个方法涉及隐式转换,因为他对基本类型值不适用,例如22,所以JS引擎会自动为22创建一个封装对象然后对该对象调用toString()

+这个一元运算符也可以将字符串转化为数字,如:

 var a = "3"
 var b = +a  //3

-这个一元操作符和+一样,不过他还会反转数字的符号位,我们不能使用--来撤销符号位的反转,而应该使用- -'3'来撤销符号位反转

 var c = "3"
 var d = -c  //-3
 var e = --c //2
 var f = - -c    //3

复杂例子:

 1 + - + + + - + 1   //2

+可以将字符串转化为数字,所以他还有一个用途:就是将日期对象强制转化为数字

 var d = new Date()  //Sun Aug 07 2022 21:46:23 GMT+0800 (中国标准时间)
 +d //1659879983031

所以获取时间戳的方法我们可以总结如下:

  • +new Date()
  • +new Date
  • new Date().getTime()
  • Date.now()

接下来,我们来了解一下另外一个运算符: ~

按位非运算符~反转操作数的位,转化为十进制,会得到一个效果: ~x大致等同于-(x+1)

而这个-(x+1)运算是一个十分特殊的运算,因为我们可以发现,当且仅当x=-1的时候,这个式子才会等于0(位运算不存在-1,所以实际上应该是0),也就是说,只有x=-1的时候,~x才会返回假值0,其他情况则返回真值

这个情况我们可以运用在很多地方,因为JS中很多地方都是使用-1作为哨位值的,比如:indexOf()找不到的时候则返回-1,所以我们可以用~代替判断

 var a = 'Hello World'
 if(a.indexOf('lo') >= 0){}  //true,找到匹配
 if(~a.indexOf('lo')){}  //true,找到匹配,用这种表达代替
 if(!~a.indexOf('ol')){} //true,找不到匹配

~运算符不仅能够代替判断,还可以做字位截除,截除数字值的小数部分

使用 ~~能够截除数字值的小数部分,第一个~执行的是ToInt32并反转字位,第二个~是在进行一次字位反转,即反转回原值

但是他对负数的处理结果和Math.floor却不太一样(正数两个都不会四舍五入)

 Math.floor(-49.6)   //-50,会四舍五入
 ~~-49.6 //-49

显示解析数字字符串

前面我们介绍了怎么将字符串显式强制类型转换为数字,现在我们来了解一下解析字符串

这两者还是有明显的区别:

 var a = '42'
 var b = '42px'
 Number(a)   //42
 parseInt(a) //42
 Number(b)   //NaN
 parseInt(b) //42

可以看到,解析允许字符串中出现非数字字符解析会按从左到右的顺序如果遇到非数字字符就停止,而转换不允许出现非数字字符,否则会失败返回NaN

解析字符串中的浮点数可以使用**parseFloat**函数

parseInt传入的参数必须是字符串值如果传入的不是字符串,则会先被强制类型转换为字符串(toString等)

我们再看看parseInt一些奇怪的地方:

parseInt(1/0, 19)会返回18,其实是因为**parseInt会先将参数强制类型转化为字符串在进行解析**,所以**1/0也就是Infinity**,转换为字符串为'Infinity' ,而基数19限定了数字的有效字符范围为0~9a~i,所以parseInt('Infinity', 19)第一个字符为I,以19为基数即为18,第二个字符为n,不是一个有效的数字字符,所以解析到此为止,输出18

 parseInt(0.000008)  //0,“0”来自于"0.000008"
 parseInt(0.0000008) //8,"8"来自"8e-7"
 parseInt(parseInt, 16)  //15,"fa来自于"false"
 parseInt(false, 16) //250,"f"来自于"function "
 parseInt("0x10")    //16
 parseInt('103', 2)  //2

显示转换为布尔值

同样,我们可以使用Boolean将非布尔值强制转化为布尔值,但是并不常用

我们常用的是使用一元运算符!显式的将强制类型转化为布尔值,但是同时还将真值与假值相互反转,所以我们常用的方法就是 !!

if这样的布尔值上下文中,不使用Boolean!!,就会自动隐式地进行布尔值的转换

隐式强制类型转化

隐式强制类型转换的作用是减少冗余,让代码更简洁

字符串和数字之间的隐式转换

前面我们了解了+运算符,他能够显式将字符串转化为数字

而现在,+不仅能够用于数字加法,还可以用于字符串拼接

 var a = '42'
 var b = "0"
 var c = 42
 var d = 0
 a + b   //'420'
 c + d   //42

我们可以看到JS根据不同情况执行了不同的操作,所以接下来我们来看看什么情况下才会执行拼接操作

  • 如果每个操作数是字符串或者能够通过以下步骤转化为字符串的话,+将进行拼接操作

  • 如果其中一个操作数是对象,则首先调用其ToPrimitive抽象操作,也就是先检查有没有valueOf,没有的话再检查toString,如果能够转化成字符串,则采用拼接方式

     var a = [1, 2]
     var b = [3, 4]
     a + b   //"1,23,4"
    

但是我们在日常开发中,通常不会这么干,下面这种形式才是我们经常使用的:

  var a = 22
  var b = a + ''
  b //"22"

在这里有一个小注意点:a+''和前面的String(a)有一个差别前一种方法会先调用valueOf,然后在通过toString抽象操作将返回值转化为字符串,而**String(a)则是直接调用ToString**

除了+以外,还有-运算符经常用于强制类型转换,例如**a - 0会强制将a转化为数字**

为了执行减法操作,操作数都需要被转化为数字,首先会被通过toString转化为字符串,然后再转化为数字

布尔值到数字的隐式转化

这种情况并不多见,这里举个例子:

需要检查函数参数中包含的true的个数:

 function onlyOne(){
     var sum = 0
     for(var i = 0; i < arguments.length; i++){
         if(arguments[i]){
             sum += arguments[i] //此处把布尔值转化为数字了
         }
     }
     return sum == 1
 }
 onlyOne(false, true, false) //true

隐式转化为布尔值

下面几种情况都会发生布尔值的隐式转化:

  • if语句中的条件判断表达式
  • for循环语句中的第二个表达式
  • whiledo...while循环的条件判断
  • 三元运算符的条件判断表达式
  • 逻辑运算符||&&的左边的操作数

在以上这些情况中,非布尔值会被隐式强制转化为布尔值,遵循前面的ToBoolean抽象操作规则

|| 和 &&

这两个是逻辑运算符,但是实际上,他们应该被更加准确的命名为选择器运算符,因为他们返回的不是布尔值,而是两个操作数中的其中一个

两个操作符都会对第一个操作数进行条件判断,如果不是布尔值则进行ToBoolean进行强制转化

对于 || 来说,如果条件判断结果为true则返回第一个操作数的值,如果为**false则返回第二个操作数的值**

对于 && 来说,如果条件判断结果为true则返回第二个操作数的值,如果为**false则返回第一个操作数的值**

这两个操作符的用处非常多,比如:

  • ||可以用来设置默认值

     function foo(a){
         a = a || 'hello'    //a不存在则设置默认值为hello
     }
    
  • &&可以用来当做守护运算符,即前面的表达式为后面的表达式把关

     var a = 22
     a && foo()  //a存在才执行函数foo
    

宽松相等和严格相等

宽松相等==和严格相等===的区别在于:==允许在相等比较中进行强制类型转换,而===不允许

所以理论上,==由于要进行强制类型转换,所以在比较不同类型的值的时候,==应该会慢一点,但是这种差别仅仅是微秒级别,所以我们不用考虑性能

抽象相等

以下的比较都是==

  • 字符串和数字之间的相等比较

     42 === '42' //false
     42 == '42'  //true
    

    如果是严格相等,则不会进行强制类型转换,所以a===b等于false

    如果是宽松相等,则会进行类型转换,转换规则如下:

    1. 如果Type(x)为数字,Type(y)为字符串,则返回x == ToNumber(y)的结果
    2. 如果Type(x)为字符串,Type(y)为数字,则返回ToNumber(x) == y的结果

    总而言之,就是把字符串转化为数字

  • 其他类型和布尔类型之间的相等比较

     var a = '42'
     var b = true
     a == b  //false
    

    此处就有一个坑, 使用宽松相等得到的结果不是true

    这里我们来了解一下类型转化规则:

    1. 如果Type(x)是布尔类型,则返回ToNumber(x) == y的结果
    2. 如果Type(y)是布尔类型,则返回x == ToNumber(y)的结果

    总而言之,就是把布尔值转化为数字

    所以在上面这段代码中,b会先被转化成1,然后'42'1比较,在把'42'转化为42,所以最终比较的就是421,故返回false

    所以我们不要使用== true== false的形式

  • null和undefined之间的相等比较

    nullundefiend之间也涉及隐式强制类型转化,规范规定:

    1. 如果xnullyundefined,则结果为true
    2. 如果xundefinedynull,则结果为true

    也就是说,==中,nullundefined相等,他们也与自身相等,除此之外其他值都不和他们相等

     var a = null
     var b
     a == b  //true
     a == null //true
     b == null //true
     a == false //false
    
  • 对象和非对象之间的相等比较

    对于对象和基本类型之间的相等比较,规范规定:

    1. 如果Type(x)是字符串或数字,Type(y)是对象,则返回x == ToPrimitive(y)的结果
    2. 如果Type(x)是对象,Type(y)是字符串或数字,则返回ToPrimitive(x) == y的结果
    3. 如果存在布尔值,就会先把布尔值转化为数字

    除此之外,我们之前还了解过封装对象,如果在宽松比较中遇到这种情况,则**==中的ToPromitive强制类型转化会将这个封装对象拆封,返回其基本数据类型值**

     var a = 'abc'
     var b = Object(a)   //与new String(a)一样
     a === b //false
     a == b  //true
    

    但是也有一些值不这样,因为==算法中其他优先级更高的规则:

    nullundefined没有相应的封装对象,所以**Object返回一个常规空对象**

    NaN可以封装为数字封装对象,拆封之后是**NaN == NaN返回false**

     var a = null
     var b = Object(a)   //和Object一样
     a == b  //false
     ​
     var c = undefined
     var d = Object(c)   //和Object一样
     c == d  //false
     ​
     var e = NaN
     var f = Object(e)   //和new Number(e)一样
     e == f  //false
    

少见的情况

  • 返回其他数字

     Number.prototype.valueOf = function(){
         return 3
     }
     new Number(2) == 3  //true
    

    由于Number(2)涉及ToPrimitive强制类型转换,因此会调用valueOf

    并且**valueOf在其他情况下还可能产生副作用**:

     if(a == 2 && a == 3){}
    

    这是一个比较细的面试题,这其实可以实现让判断为true,只要设置valueOf函数即可

     var i = 2
     Number.prototype.valueOf = function(){
         return i++
     }
     var a = new Number(22)
     //这就能使上面的判断成立
    
  • 假值的相等比较

    这里分别列出常规和非常规的假值相等比较

    比较等式比较结果备注
    "0" == nullfalsenull除了nullundefined,其他值比较都为false
    "0" == undefinedfalseundefined除了nullundefined,其他值比较都为false
    "0" == falsetruefalse会先转化为0
    "0" == NaNfalse字符串"0"会转化为数字0
    "0" == 0true字符串"0"会转化为数字0
    "0" == ""false依次比较每个字符是是否相等
    false == nullfalsenull除了nullundefined,其他值比较都为false
    false == undefinedfalseundefined除了nullundefined,其他值比较都为false
    false == NaNfalse任何值与NaN比较都为false,并且包括其自身
    false == 0truefalse会转化为0
    false == ""truefalse会转化为0,然后''会转化为0
    false == []truefalse会转化为0,然后[]会转化为0
    false == {}falsefalse会转化为0,然后{}会转化为NaN
    "" == nullfalsenull除了nullundefined,其他值比较都为false
    "" == undefinedfalseundefined除了nullundefined,其他值比较都为false
    "" == NaNfalse任何值与NaN比较都为false,并且包括其自身
    "" == 0true''会转化为0
    "" == []true''先转化为0,然后[]再转化为0
    "" == {}false''先转化为0,然后{}再转化为NaN
    0 == nullfalsenull除了nullundefined,其他值比较都为false
    0 == undefinedfalseundefined除了nullundefined,其他值比较都为false
    0 == NaNfalse任何值与NaN比较都为false,并且包括其自身
    0 == []true[]转化为0
    0 == {}false{}转化为NaN

    总结出来的判断规则:

    1. null除了nullundefined,其他值比较都为false
    2. undefined除了nullundefined,其他值比较都为false
    3. 任何值与NaN比较都为false,并且包括其自身
    4. 两个字符串比较就依次比较每个字符是是否相等
    5. false绝大部分情况下需要化为0
    6. []转化为数字会转化为0
    7. {}转化为数字会转化为NaN
  • 极端例子

    [] == ![]

    这个例子会返回true,因为!运算符,根据ToBoolean规则,他会进行布尔值的强制转化,所以[] == ![]变成了[] == false,所以也就是返回true

    2 == [2]

    这个例子会返回true,因为[2]会进行强制类型转化,转化为"2"

    "" == [null]

    这个例子也会返回true,因为根据ToPrimitive规则,[null]会直接转化为"",而不是转化为"null",因为[null]在显式强制转化中也是转化为""

    0 == "\n"

    这个例子也返回true,因为"""\n"(或者其他" "等空格组合)等空字符串被ToNumber强制类型转化为0

抽象关系比较

规范中定义了抽象关系比较,分为两个部分:比较双方都是字符串和其他情况

  • 双方不都是字符串

    比较双方都先调用ToPrimitive如果出现非字符串,就根据ToNumber规则将双方强制类型转换为数字来比较

     var a = [42]
     var b = ["43"]
     a < b   //true
     b < a   //false
    
  • 双方都是字符串

    按照字母顺序来比较

     var a = ["42"]
     var b = ["043"]
     a < b   //false
    

这就是比较的基本规则,但是还有一些奇怪的例子:

 var a = {b: 42}
 var b = {b: 43}
 a < b   //false
 a == b  //false
 a > b   //false
 a <= b  //true
 a >= b  //true

上述代码中的第3和第5行打印false是因为这两个对象都会转化为[object Object],所以字符串比较都相等,故这两条式子为false

而第4行打印false是因为这是两个不同的对象在比较,所以打印false

而最后两行打印true的原因是:根据规范,a <= b会被处理为b < a,然后再将结果反转,因为b < a的结果为false,所以a <= bD的结果是true

也就是说,JS中的<=是不大于的意思

语法

语句和表达式

语句的结果值

每一条语句其实都有结果值,但是规范定义**var的结果值是undefined**

这里其实要更复杂一些,规范中的变量声明算法实际上有一个返回值,是一个包含所声明变量名称的字符串

但是这个值被变量语句算法屏蔽掉了for...in循环除外),最后返回结果为空(undefined

那么其他语句的结果值呢?

比如代码块{...}的结果值就是其最后一个语句/表达式的结果

 var a, b
 a = if(true){
     b = 4 + 32
 }

上述这段代码的本意是通过if代码块的最后一条语句的返回值赋值给a

但是事实上这样会报错,因为语法不允许我们获得语句的结果值并赋值给另一个变量

那现在我们应该怎么获取语句的结果值呢?

有两种方法可以获取:

  • eval

    使用eval函数来获得结果值

     var a, b
     a = eval('if(true){b = 4 + 32}')
     a   //36
    

    但是我们一般不使用eval函数,所以这个方法很少用

  • do表达式

    do表达式是ES7中的一项提案,但是并未被广泛应用

     var a, b
     a = do {
         if(true){
             b = 4 + 32
         }
     }
     a   //36
    

上下文规则

大括号
  • 对象常量

    通常我们定义一个变量都会使用大括号

  • 标签

    这种情况比较少见,比如:

     {
         foo: bar()
     }
    

    这段代码中的大括号并不是代表一个对象,而是代表一个代码块,与if条件语句中的代码块作用基本相同

    接下来,我们看一下foo: bar(),这个语句在代码块中其实是一个标签语句,foobar()的标签

    由于JS没有goto语句,所以不能随意让代码跳转到指定位置执行,这是一种糟糕的编码方式,但是在某些情况下,这个语法也大有用处

    所以JS没有完全实现这个goto语句,但是能够通过标签跳转实现goto的部分功能,continuebreak都可以带一个标签

    这个功能最大的用处在于可以从内存循环跳转到外层循环

     foo: for(var i = 0; i < 4; i++){
         for(var j = 0; j < 4; j++){
             if((i * j) >= 3){
                 console.log("stopping!", i, j)
                 break foo
             }
             console.log(i, j)
         }
     }
     //0 0
     //0 1
     //0 2
     //0 3
     //1 0
     //1 1
     //1 2
     //stopping! 1 3
    
代码块
 [] + {} //'[object Object]'
 {} + [] //0

这段代码极具疑惑性

第一行代码中, {}出现在+运算符表达式中,所以他被当成一个对象来处理,会被强制类型转化为'[object Object]' ,而 []会被强制转化为""

第二行代码中, {}被当作一个独立的空代码块,所以相当于执行+[] ,最后+[][]转化为0

else if

我们通常会使用else if语句,但是实际上,JS并没有这个语句,我们经常用到的else if实际上是这样的:

 if(a){
     //...
 }else{
     if(b){}else{}
 }

try...finally

finally中的代码总是会在try后面执行,如果有catch就会在catch后面执行

但是**try中如果有return语句**,那么情况将会发生改变

 function foo(){
     try{
         return 42
     }finally{
         console.log('Hello')
     }
 }
 console.log(foo())
 //Hello
 //42

可以看到,return 42先执行。将函数的返回值设置为42,然后try执行完毕,接着执行finally,最后foo函数执行完毕,打印返回值

同样,throw也是一样

如果**finally中抛出异常,那么函数就会在此处终止**,如果此前try中已经有return设置了返回值,则该值会被丢弃

并且**finally中的return会覆盖trycatchreturn的返回值**

switch

switch中的case表达式的匹配算法与===相同

 var a = 'hello'
 var b = 10
 switch(true){
     case(a || b == 10):
         //执行不到这里
         break
     default: 
         console.log('aaa')
 } 
 // aaa

\