【JS系列2】类型和语法

200 阅读14分钟

类型和语法

类型

内置类型

  • null
  • undefined
  • boolean
  • number
  • string
  • object
  • symbol
typeof undefined === "undefined"; // true 
typeof true === "boolean"; // true 
typeof 42 === "number"; // true 
typeof "42" === "string"; // true 
typeof { life: 42 } === "object"; // true 
// object 子类型
typeof function a () {} === "function" // true
typeof [1, 2, 3] === "object" // true

// ES6中新加入的类型 
typeof Symbol() === "symbol"; // true

// 以上六中均有同名字符串与之对应
// null 特殊
typeof null === "object" // true

值和类型

变量是没有类型的, 使用typeof 对变量操作时是针对变量所持有值的操作。

undefined&undeclared

undefined 是值的一种,undeclared则表示变量还没有被声明过。

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

typeof a // undefined
typeof b // undefined

typeof 防御机制

尤其是多个脚本文件共享全局命名空间加载变量

  • 判断未申明变量

    // 会报错
    // Uncaught ReferenceError: DEBUG is not defined
    if (DEBUG) {
        console.log("DEBUG is starting")
    }
    
    • typeof
    if (typeof DEBUG !== "undefined") {
         console.log("DEBUG is starting")
     }
    
    • window
     if (window.DEBUG) {
         console.log("DEBUG is starting")
     }
    

数组

tips

  • delete 可以删除数组,但并不会改变length
  • 创建"稀疏数组"(含有空白或空缺单元的数组),空缺单元的undefined与显示赋值的undefined不同
  • 数组通过数字索引,索引也是对象,所以可以包含字符串键值和属性。如果字符串键值能够被强制转换为十进制数组的话,也会被当作数字索引来处理。
var a = []
a[0] =1
a['footer'] = 2

console.log(a.length) // 1
console.log(a['footer']) // 2
console.log(a.footer) // 2

a['13'] = 14 
console.log(a.length) // 14

类数组

一组可以挺过数字索引的值如: DOM查询操作返回的DOM元素列表、通过arguments对象将函数的参数当作列表访问

  • 类数组转换为数组
function foo () {
    console.log(arguments) // {"0":"bar","1":"baz","2":"bam"}
    let arr = Array.prototype.slice.call(arguments)
    console.log(arr) // ["bar","baz","bam"]
}
let obj = {}
foo.apply(obj, ['bar','baz', 'bam'])
  • ES6 Array.from

字符串

字符串在某种程度上可以说是类数组,都有length、indexof、concat。但是JS中字符串是不可变的,数组是可变的。因此字符串无法“借用”数组的可变成员函数如:reverse

数字

整数

"整数"就是没有小数的十进制数

console.log(42.0 === 42) // true

Number 方法

var a = 42.59; 
a.toPrecision( 1 ); // "4e+1" 
a.toPrecision( 2 ); // "43" 
a.toPrecision( 3 ); // "42.6" 
a.toPrecision( 4 ); // "42.59" 
a.toPrecision( 5 ); // "42.590" 
a.toPrecision( 6 ); // "42.5900"

// 无效语法: 
42.toFixed( 3 ); // SyntaxError  42.被视为一部分

// 下面的语法都有效: 
(42).toFixed( 3 ); // "42.000" 
0.42.toFixed( 3 ); // "0.420" 
42..toFixed( 3 ); // "42.000"

支持格式

二进制、八进制、十六进制

0xf3; // 243的十六进制 
0Xf3; // 同上

0o363; // 243的八进制 
0O363; // 同上 
0b11110011; // 243的二进制 
0B11110011; // 同上
 

0.1+0.2 === 0.3

JS的"机器精度":2^-52,在ES6中该值定义在Number.EPSILON中,可以拿来使用,可以用来比较两个数字是否相等。

function numberCloseToEqual (a, b) {
    return Math.abs(a - b) < Number.EPSILON
}

let a = 0.1 + 0.2
let b = 0.3
console.log(numberCloseToEqual(a, b)) // true
console.log(numberCloseToEqual(0.000000001, 0.000000000002)) // false

整数的安全范围

数字呈现方式决定了“整数”的安全范围需要远远小于Number.Max_value(1.7976931348623157e+308)。能够被"安全"呈现的最大整数是2^53-1, 在ES6中被定义为Macth.MAX_SAFE_INTEGER,相对应的最小整数位Number.MIN_SAFE_INTEGER

整数检测

  • ES6中的Number.isInteger()

    
    function print(val) {
        console.log(val)
    }
    print(Number.isInteger( 42 )) // true 
    print(Number.isInteger( 42.000 )) // true 
    print(Number.isInteger( 42.3 )) // false
    

    为ES6之前的版本实现Number.isInteger()

    if (!Number.isInteger) {
        Number.isInteger = function (number) {
            return typeof number === 'number' && number % 1 == 0
        }
    }
    
  • 检查是否为安全整数 Number.isSafeInteger(...),比Number.isInteger多一个不大于最大安全值的判断

32位有符号整数

所有的按位操作符都只适用于32位数字,这种操作下数字的安全范围就变成了Math.pow(-2, 31)~Math.pow(2, 31);如:|、&、<<、>> 按位操作符详细说明

特殊数值

不是值的值

undefined 类型只有一个值,即 undefined。null 类型也只有一个 值,即 null。它们的名称既是类型也是值。

undefined 和 null 常被用来表示“空的”值或“不是值”的值。二者之间有 一些细微的差别。例如:

  • null 指空值(empty value)
  • undefined 指没有值(missing value) 或者:
  • undefined 指从未赋值
  • null 指曾赋过值,但是目前没有值

null 是一个特殊关键字,不是标识符,我们不能将其当作变量来使用和赋 值。然而 undefined 却是一个标识符,可以被当作变量来使用和赋值。

void

通常情况下可以通过void 0来获取undefined

特殊数字

  • NaN 可以理解为'无效数值'、'失败数值'。typeof NaN === 'number'为true;NaN == NaN为false.

    • 可以使用全局isNaN()检查是否为NaN,但事实上检查结果并不准确,实际上isNaN()是检查参数是否不是NaN, 也不是数字。
    window.isNaN(NaN) // true
    window.isNaN('foo') // true
    window.isNaN(2) // false
    
    • ES6可以使用Number.isNaN()
    Number.isNaN(NaN) // true
    Number.isNaN('foo') // false
    Number.isNaN(2) // false
    

    内置实现方法

    if (!Number.isNaN) {
        Number.isNaN = function (n) {
            return n !== n
        }
    }
    
  • 无穷数 Number.POSITIVE_INFINITY Number.NEGATIVE_INFINITY

console.log(1 / 0, -1 / 0) // Infinity,-Infinity
console.log(Infinity / Infinity) // NaN
  • 零值

简单标量基本类型值(字符串和数字等)通过值复制来赋值 / 传递,而复合 值(对象等)通过引用复制来赋值 / 传递。JavaScript 中的引用和其他语言 中的引用 / 指针不同,它们不能指向别的变量 / 引用,只能指向值。

原生函数

  • 常用原生函数: String() Number() Boolean() Array() Object() Function() RegExp() Date() Error() Symbol()
let a = new String('abc')  // 创建字符串'abc'的封装对象 而非基本类型值'abc'
console.log(a) 
// String {"abc"}
    0: "a"
    1: "b"
    2: "c"
    length: 3
    __proto__: String
    [[PrimitiveValue]]: "abc"
console.log(typeof a) // object
console.log(a instanceof String) // true
console.log(Object.prototype.toString.call(a)) // [object String]

内部属性[[class]]

所有typeof返回值为"object"的对象(如数组)都包含一个内部属性[[Class]],但是这个属性无法直接访问,一般通过Object.prototype.toString()来查看。

Object.prototype.toString.call( [1,2,3] ); // "[object Array]" 
Object.prototype.toString.call( /regex-literal/i ); // "[object RegExp]"
Object.prototype.toString.call( null ); // "[object Null]"
Object.prototype.toString.call( undefined ); // "[object Undefined]"
Object.prototype.toString.call( "abc" ); // "[object String]" 
Object.prototype.toString.call( 42 ); // "[object Number]" 
Object.prototype.toString.call( true ); // "[object Boolean]"

数组内部[[Class]]属性值是Array, 正则表达式的值是RegExp。

虽然 Null() 和 Undefined() 这样的原生构造函数并不存在,但是内部 [[Class]] 属性值仍然是 "Null" 和 "Undefined"。

封装对象包装

JS 会自动为基本类型值包装一个封装对象

封装对象释疑

console.log(new Boolean(false))
if (new Boolean(false)) { // 一定会执行
    console.log(1)
} 

拆封

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

var a = new String( "abc" ); 
var b = new Number( 42 ); 
var c = new Boolean( true ); 
a.valueOf(); // "abc" 
b.valueOf(); // 42 
c.valueOf(); // true 

隐式拆分(强制类型转换):

var a = new String( "abc" ); 
var b = a + ""; // b的值为"abc" 

typeof a; // "object" 
typeof b; // "string" 

强制类型转换

可以这样来区分:类型转换发生在静态类型语言的编译阶段,而强制类 型转换则发生在动态类型语言的运行时(runtime)。

抽象值操作

ToString

  • 对普通对象来说,除非自行定义,否则 toString() (Object.prototype.toString())返回内部属性 [[Class]] 的值 ,如 "[object Object]"。 如果对象有自己的 toString() 方法,字符串化时 就会调用该方法并使用其返回值。

  • 数组调用默认toString()经过了重新定义,将所有单元字符串化以后再用","连接起来,如:

var a = [1, 2, 3]
console.log(a) // "1, 2, 3"

Tips: JSON 字符串化

  • JSON.stringify() 与 ToString 相同点
    • 字符串、数字、布尔值和 null 的 JSON.stringify(..) 规则与 ToString 基本相同。
    • 如果传递给 JSON.stringify(..) 的对象中定义了 toJSON() 方 法,那么该方法会在字符串化前调用,以便将对象转换为安全的 JSON 值。

所有安全的 JSON 值(JSON-safe)都可以使用 JSON.stringify(..) 字 符串化。安全的 JSON 值是指能够呈现为有效 JSON 格式的值。

  • 不安全JSON值

undefined、 function、symbol(ES6+)和包含循环引用(对象之间相互引用,形成 一个无限循环)的对象。JSON.stringify(..) 在对象中遇到 undefined、function 和 symbol 时会自动将其忽略,在数组中则会返回 null(以保证单元位置不 变)。

  • 对象中定义了toJSON方法,JSON 字符串化时会首先调用该方 法,然后用它的返回值来进行序列化。

ToNumber

处理失败时返回NaN,同时针对十六进制数据按照十进制处理。

二进制: 0b/0B; 八进制:0o/0O; 十六进制: 0x/0X

var a = { 
    valueOf: function(){ 
        return "42"; 
    } }; 
var b = {
    toString: function(){ 
        return "42"; 
    }
}; 
var c = [4,2]; 
c.toString = function(){ 
    return this.join( "" ); // "42" 
}; 


Number( a ); // 42
Number( b ); // 42 
Number( c ); // 42 
Number( "" ); // 0 
Number( [] ); // 0 
Number( [ "abc" ] ); // NaN

let num = '0x123'
console.log(Number(num)) // 291

ToBoolean

  • 假值
    • undefined
    • null
    • false
    • +0、-0和NaN
    • ''

ToPrimitive

为了将值转换为相应的基本类型值,抽象操作 ToPrimitive(参见 ES5 规范 9.1 节)会首先(通过内部操作 DefaultValue,参见 ES5 规范 8.12.8 节)检查该值是否有 valueOf() 方法。如果有并且返回基本类型 值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回 值(如果存在)来进行强制类型转换。

如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。

显示强制类型转换

  • Number、String
  • toString
  • 一元操作符+ 、-

可常用操作

  • 日期显示转换为数字
    var timestamp = +new Date()
    
    var timestamp = new Date().getTime(); 
    // var timestamp = (new Date()).getTime(); 
    // var timestamp = (new Date).getTime();
    
    // Best Es5
    var timestamp = Date.now()
    
  • ~运算符 上面提到过二进制运算符,也为其中一个。遇到时首先将值强制转换为32位数字,然后执行字位操作“非”(对每个字符进行翻转)

~X 大致等同于-(x+1)

~42 // -(42+1) ===> -43

接下来先介绍下 indexOf 的返回值,失败为-1。在C语言中-1为"哨位值,被赋予了特殊的意义, -1代表执行失败,大于-1为成功。在JS中遵循了这一惯例。

优化indexOf判断

let a = "Hello World"; 
console.log(~a.indexOf( "lo" )); // -4 <-- 真值!
console.log(~a.indexOf( "ol" )); // 0 <-- 假值!
  • 学位截除 ~~ 因为进行了两次反转,所以相当于执行ToInt32后的结果

~~ X 相当于能将值截取为一个32位整数

显示解析数字字符串

解析数字字符串是允许字符串中含有非数字字符,解析从左到右,如果遇到非数字字符就停止。而转换不允许出现非数字字符,否则会失败并返回NaN。

a = "42"
b = "42px"

console.log(Number(a), Number(b)) // 42 NaN
console.log(parseInt(a), parseInt(b)) // 42 42

parseInt(..) 先将参数强制类型转换为字符串再进行解析

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

显示转换为Boolean

Boolean(X) 或者 !!X

隐私类型转换

Tips

当一个操作数为对象时,首先会对其调用ToPrimitive操作,其次会再调用[[DefaultValue]],以数字作为上下文, 详细解释见ToPrimitive。

ToNumber抽象操作处理对象方式类似。例如:数组的valueOf()操作无法的得到简单基本类型值,就会转而调用toString()

字符串和数字之间的隐式强制类型转换

  • 操作符的其中一个操作数是字符串或者可以得到字符串,则执行字符串拼接;否则执行数字加法。

隐式强制类型转换布尔值

  • if (..) 语句中的条件判断表达式。
  • for ( .. ; .. ; .. ) 语句中的条件判断表达式(第二个)。
  • while (..) 和 do..while(..) 循环中的条件判断表达式。
  • ? : 中的条件判断表达式。
  • 逻辑运算符 ||(逻辑或)和 &&(逻辑与)左边的操作数(作为条件判 断表达式)。

|| 和 &&

|| 和 && 并不是真正意义上的“逻辑运算符”,确切的更贴近与“选择器操作符”或“操作数选择器运算符”。因为他们返回的并不是布尔值,返回值是两个操作数中的一个。

var a = 42; 
var b = "abc"; 
var c = null; 
console.log(a || b, a && b, c || b, c && b) // 42,abc,abc,null

对于 || 来说,如果条件判断结果为 true 就返回第一个操作数(a 和 c) 的值,如果为 false 就返回第二个操作数(b)的值。

&& 则相反,如果条件判断结果为 true 就返回第二个操作数(b)的值, 如果为 false 就返回第一个操作数(a 和 c)的值。

符号的强制类型转换

ES6 允许从符号到字符串的显式强制类型转换,然而隐式强制类型 转换会产生错误。

符号不能够被强制类型转换为数字(显式和隐式都会产生错误),但可以 被强制类型转换为布尔值(显式和隐式结果都是 true)。

=== 和 ==

相等性判断官方详解

“== 允许在相等比较中进行强制类型转换,而 === 不允 许。”

性能

=== 和 == 都会检查操作数的类型,== 类型不同时会进行强制类型转换,性能差别微秒级可忽略。实际上比较两个对象时,两者工作原理一样;两个对象指向同一个值时即视为相等,并不发生强制类型转换。

相等比较

  • 数字和字符串

    a = 42
    b = "42"
    
    console.log(a === b) // false
    console.log(a == b) // true
    
    • a==b 具体是如何转换?

      ES5规范

      • 如果 a 是数字,b是字符串,则返回 a == ToNumber(b)
      • 如果 a 是字符串, b是数字, 则返回 ToNumber(a) = b
  • 其他类型和布尔类型

    b = "42"
    
    console.log(b == true) // false
    

    ES5规范

    • 如果a 是Boolean, 则返回 ToNumber(a) == b
    • 如果b 是Boolean,则返回 a == ToNumber(b)
  • null 和 undefined

    在 == 中 null 和 undefined 相等(它们也与其自身相等),除此之外其 他值都不存在这种情况。

    var a = null; 
    var b; 
    a == b; // true 
    a == null; // true 
    b == null; // true 
    a == false; // false 
    b == false; // false 
    a == ""; // false 
    b == ""; // false 
    a == 0; // false 
    b == 0; // false
    
  • 对象和非对象

    ES5规定: == 操作符两边是对象的一侧进行 ToPrimitive操作(仅限于字符、数组),布尔类型会先被强制转换为数字

a = 42
b = [42]

a == b // true

c = "obc"
d = Object(c)
c == d
  • 其他
    • 更改内置原生原型

      Number.prototype.valueOf = function () {
        return 3
      }
      
      console.log(new Number(2) == 3) // true
      
      
    • 面试题 如何实现a == 2 && a == 3

      // 使用valueOf
      let i = 2
      Number.prototype.valueOf = function () {
          return i++
      }
      var a = new Nummber(2)
      if (a == 2 && a == 3) {
          // true
      }
      
      // 使用defineProperty劫持
      let obj = {
          a: 1
      }
      Object.defineProperty(obj, 'a', {
          get () {
              return i++
          }
      })
      
      
    • 常见 == 判断

      "0" == null; // false 
      "0" == undefined; // false 
      "0" == false; // true -- 晕! 
      "0" == NaN; // false 
      "0" == 0; // true 
      "0" == ""; // false 
      
      false == null; // false 
      false == undefined; // false 
      false == NaN; // false 
      false == 0; // true -- 晕! 
      false == ""; // true -- 晕! 
      false == []; // true -- 晕! 
      false == {}; // false 
      
      "" == null; // false 
      "" == undefined; // false 
      "" == NaN; // false 
      "" == 0; // true -- 晕! 
      "" == []; // true -- 晕! 
      "" == {}; // false
      
      0 == null; // false 
      0 == undefined; // false 
      0 == NaN; // false 
      0 == []; // true -- 晕!
      
    • 极端情况

      2 == [2] // true
      [] == ![] // true
      "" == [null] // true
      0 == "\n" // true
      

抽象关系比较

a <= b 会被处理为 !(b < a) 来判断

  • 比较双方均为字符串:按照字母顺序进行比较
  • 其他:首先调用ToPrimivite,如果出现非字符串,就根据ToNumber强制类型转换为数字进行比较

语法

语句和表达式

语句相当于能完整表达意思的句子,并有一组词或者N个表达式组成,它们由运算符连接。其中表达式相当于短语,表达式可以由更小的表达式组成,可以有完整的意思也可以没有。

语句的结果值

语句均有一个结果值,其中规定定义 var 的结果值是 undefined

语法中不允许我们将语句结果只赋值给另一个变量, 如:

var a, b
a = if (true) {
    b = 4+38
} // if 代码块在控制台输出42,但是赋值给另一个变量是不被允许的

但是在实际过程中我们可以通过其他方式得到语句的执行结果:eval() 强烈不推荐、ES7中的do{...}表达式执行代码块

表达式副作用

可以赋值语句的结果值合并判断条件

上下文规则

  • {...} 由此引出JS一个不太为认知的特性(不建议使用),标签语句
    foo: for(let i = 0; i < 4; i++) { // foo 标签
        for (let j =0; j < 4; j++) {
            if (i == j) {
                console.log('跳过的', i, j)
                // 跳转到foo 的下一个循环 i+1
                continue foo
            }
    
            if ((i * j) % 2 == 1) {
                console.log('跳过奇数', i, j) 
                // 跳过本次循环,j+1
                continue 
            }
    
            console.log('未跳过' ,i, j)
        }
    }
    
    带标签循环跳转更大的用处在于:和break一起使用实现从内层跳转到外层
    foo: for(let i = 0; i < 4; i++) {
        for (let j =0; j < 4; j++) {
            if ((i * j) % 2 == 1) {
                console.log('跳过奇数', i, j)
                break foo // 跳出标签foo 所在的循环和代码块
            }
    
            console.log('未跳过' ,i, j)
        }
    }
    

Tips

[] + {} // "[object Object]"
{} + [] // 0

第一行代码中,{} 出现在 + 运算符表达式中,因此它被当作一个值(空对 象)来处理。第 4 章讲过 [] 会被强制类型转换为 "",而 {} 会被强制类型 转换为 "[object Object]"。

但在第二行代码中,{} 被当作一个独立的空代码块(不执行任何操作)。 代码块结尾不需要分号,所以这里不存在语法上的问题。最后 + [] 将 [] 显式强制类型转换(参见第 4 章)为 0。

冷知识

  • 事实上JS中是没有 else if的,但是 if 和 else 在只包含单条语句的时候可以省略代码块{},实际我们经常用到的else if如下:
if (a) {
} else {
    if (b) {} else {}
}

运算符优先级

运算符优先级官方详解

异步与性能