3.语言基础 - 2

73 阅读16分钟

数据类型

es6有六种简单类型:Undefined, Null, Boolean, Number, String, Symbol, 其中 Symboles6 新增的,还有一种复杂数据类型 Object, Object 是一种无序明值对的集合

typeof 操作符

因为 ECMAScript 的类型是松散的,所以需要一种手段来判断数据类型, typeof 就是为此而生的

typeof的返回值如下

  • 'undefined' 未定义
  • 'boolean' 布尔值
  • 'string' 字符串
  • 'number' 数字
  • 'object' 对象(不是函数),或者null
  • 'function' 函数
  • 'symbol' 符号(唯一值)

💡 typeof null 返回的结果是 object, 是因为 null 是对空对象的一个引用

Undefined

undefined类型的值只有一种,就是特殊值undefined,使用let或者var声明的变量但是没有赋值就是undefined

一般情况下,不要显示的给一个变量赋值为undefined,因为默认值就为undefined

在对未初始化的值判断的值为undefined,但是对未声明的变量调用,返回的值也是undefined,

    let message;
    // let age;

    console.log(typeof message) // undefined
    console.log(typeof age) // undefined
    console.log(typeof age === typeof message)  // true

无论是未声明还是声明了,typeof返回的值都是undefined。逻辑上是对的,因为严格来说,两个变量存在根本性差异,但是它们都无法进行实际的操作

Null

null值表示一个空对象指针,这就是typeof null返回object的原因

在定义将来要保存对象值的变量时,建议使用null来初始化

undefined是由null派生而来的,因此,ecmascript将他们定义为表面相等

console.log(null == undefined) // true

即使null和undefined有关系,但是他们的用途是完全不一样的,永远不要主动声明一个变量为undefined,但是null不一样,只要保存对象值,都可以用null来进行填充,这样就可以保证null是空对象指针的语义

Boolean

Boolean类型是js中使用最频繁的一个类型之一,它只有两个值,true或者false

要将一个其他的值转为boolean值,可以调用特定的Boolean()函数

下表总结了不同的值与布尔值之间的转换规则

数据类型转换为true的值转换为false的值
Booleantruefalse
String非空字符串空字符串
Number非0(包括无穷值)0,NaN
Object任意对象null
Undefined不存在undefined

Number

js中最有意思的类型也许就是Number了, 不同的数值类型相应的也有不同的数值字面量格式

整数

书写十进制整数

let num1 = 1;

整数也可以用八进制表示,对于八进制字面量,第一个数字必须是0,(在严格模式下必须是0o开头)

    let num2 = 012; // 八进制012 转十进制为 10
 
    function test() {
      "use strict";
      let n1 = 012; // error: Octal literals are not allowed in strict mode.
      let n2 = 0o12; //  正确
    }
    test()

整数也可以用十六进制表示,对于十六进制字面量,第一个数字必须是0x

   let n3 = 0x12; // 十六进制转十进制为18

浮点数

要定义浮点数,数值中必须包含小数点,而且小数点后面必须要有至少一个数字,虽然小数点前面不是必须的,但是推荐加上

        let floatNum1 = 1.1;
        let floatNum2 = 0.1;
        let floatNum3 = .1; // 有效但是不推荐

存储浮点数的使用内存是整数的两倍,所以说js总是想着把值转为整数, 在小数点后面没有数字,或者数字是0 的情况下会被转为整数

        let floatNum4 = 1.; // 转换为1
        let floatNum5 = 1.0; // 转换为1

对于非常大或者非常小的数据,浮点数可以用科学记数法表示,

      let floatNum6 = 3.12e5; //  312000

浮点数的京都最高可以达到17位小数,但是在算术计算中远不如整数精确,例如0.1 + 0.2 不等于0.3

   console.log(0.1 + 0.2 == 0.3) // false

如果两个数为0.05和0.25,或者0.15和0.15,那是没问题的,但是如果是0.1和0.2 会出现问题,所以说,永远不要测试某个特定的浮点值

之所以存在这种情况,是因为使用了 IEEE754 数值, 这种错误并非ECMAScript独有的,其他使用同格式的语言也会有

值的范围

ECMAScript 可以表示的最大值保存在 Number.MAX_VALUE 中,最小值保存在 Number.MIN_VALUE, 如果某一个值超过了这个范围,它会被自动转为一个特殊的Infinity(无穷)值,任何无法表示的负数用-Infinity表示, 正数用Infinity表示

如果计算返回正负Infinity,那么这个数将不能做进一步运算,要确定一个值是不是有限大,可以使用isFinte()函数判断

NaN

有一个特殊的值是NaN,(not a number),用于表示本来要返回数值的操作失败了(而不是抛出错误),比如,用0除任何数值

console.log(0/0) // NaN

如果分子是非0值,分母为0,则会返回Infinity或者-Infinity

    console.log(-1/0) // -Infinity
    console.log(1/0) // Infinity

NaN有几个独特的特性,涉及NaN的任何运算都会返回NaN, 其次NaN不等于包括NaN在内的任何值

   console.log(NaN == NaN) // false

为此,ECMAScript提供了isNaN这个函数来判断

    console.log(isNaN(NaN)) // true
    console.log(isNaN(10)) // false
    console.log(isNaN('10')) // false
    console.log(isNaN('blue')) // true 'blue'转数字为NaN
    console.log(isNaN(true)) // false true转数字为1

数值转换

有三个函数可以将非数值转换为数值:Number(), parseInt(), 和parseFloat()

Number()是转型函数,可以用于任何数据,后面两个主要针对字符串

Number()函数基于如下规则进行转换

  • 布尔值,true转为1,false为0
  • 数值,直接返回
  • null 返回0
  • undefined 返回NaN
  • 字符串
    • 如果字符串只包含数值字符,则转换为数值(十进制)
Number('012') // 12 
    • 如果字符串包含有效浮点数字符,转换为浮点数
    • 如果包含有效的十六进制字符,则转换为十进制
 Number('0x13') // 19 
    • 如果是空字符串,则返回0
    • 如果包含上述之外的其他字符,则返回NaN
  • 对象,调用对象valueOf()方法,并按上述规则进行转换,如果结果是NaN,则调用toString()方法在进行转换

parseInt更专注于字符串是否包含数值模式,从第一个非空的字符开始转换,如果第一个不是有效的数值字符,加号或者减号,则返回NaN, 这意味着空字符串也会返回NaN(这一点和Number不一样,它返回0)

如果字符串的第一个字符是数值字符,parseInt能识别到不同的整数格式,如果字符串是“0x”开头的,则会被解释为十六进制

        console.log(parseInt('012')) // 12
        console.log(parseInt('0x23')) // 35
        console.log(parseInt('a12')) // NaN
        console.log(parseInt('')) // NaN

不同的数值格式很容易混淆,所以parseInt提供了第二个参数,用于表示几进制进行解析

        console.log(parseInt('123', 2)) // 1  2进制遇2进一,这里123后面23是不符合的,所以解析了第一位
        console.log(parseInt('123', 8)) // 83
        console.log(parseInt('123', 10)) // 123
        console.log(parseInt('123', 16)) // 291

如果提供了十六进制参数,那么字符串前面的'0x'可以忽略掉

        console.log(parseInt('AF', 16)) // 175
        console.log(parseInt('AF')) // NaN

String

字符串可以使用单引号('),双引号("),或者反引号(`)表示

字符字面量

字符串数据类型包含一些字符字面量

字面量含义
\n换行
\t制表
\b退格
\r回车
\f换页
\反斜杠()
'单引号(')
"双引号(")
`反引号(`)
\xnn以十六进制编码nn表示的字符
\unnnn以十六进制编码nnnn表示的Unicode字符

这些字符字面量可以出现在字符串的任意位置,转义字符被视为是一个字符

let str1 = 'this is the letter sigma" \u03a3.'
console.log('str1.length',str1.length) // 28

字符串的特点

ECMAScript 中的字符串是不可变的(immutable),一旦被创建,他的值就不能变了,要修改的话,必须先销毁之前的字符串,并且赋值一个新的变量

let lang = 'java';
lang += 'script'

转换为字符串

有两种方式可以转为字符串

第一种为toString(),这个方法可以用于数值,布尔值,对象,字符字面量,注意,null和undefined没有toString()

在对数值做toString处理的时候,可以接受一个底数参数,这个参数表示按照几进制进行转换

let num = 10;
console.log(num.toString(2)) // '1010'
console.log(num.toString(8)) // '12'
console.log(num.toString(10)) // '10'
console.log(num.toString(16)) // 'a'

如果你不确定一个值是不是null和undefined,可以使用String转型函数,他始终会返回相应值的字符串

String 有如下规则

  • 如果值有toSring()方法,则调用该方法
  • 如果值是null,返回'null'
  • 如果值是undefined,返回'undefined'
console.log('String(true)',String(true)) // 'true'
console.log('String(null)',String(null)) // 'null'
console.log('String(undefined)',String(undefined)) // 'undefined'
console.log('String(123)',String(123)) // '123'

模板字符串

ES6新增的使用模板字面量定义字符串的能力,与单双引号不同的是,模板字面量保留换行字符,可以夸行定义字符串

    let str1 = 'first line \nsecond line'
    let str2 = `first line 
second line`

    console.log('str1==str2', str1 == str2) // true

模板字符串在定义模板时特别有用

let pageHTML = `
<div>
  <a href="#"></a>
</div>
`

由于模板字符串会保留换行时候缩紧的空格,所以使用的时候一定要小心

let str3 = 'first line \nsecond line'
let str4 = `first line 
            second line`
console.log('str3',str3.length) // 23
console.log('str4',str4.length) // 39

字符串插值

字符串插值通过${}中使用一个js表达式实现

let name = 'zhang san';
let str5 = 'my name is ' + name;
let str6 = `my name is ${name}`

所有插入的值都会使用toString()强制转为字符串

let name = { toString() { return 'zhang san' } };
let str6 =  `my name is ${name}`
console.log('str6',str6)

模板字面量标签函数

模板字面量也支持定义标签函数

标签函数是一个常规的函数,接收的参数依次是原始字符串数组,和对每个表达式求值的结果

let a = 1;
let b = 2;
function simpleTag(...args) {
  console.log(args)
}
simpleTag`${a} + ${b} = ${a + b}`; // ["", " + ", " = ", ""] 1, 2, 3

原始字符串

使用模板字面量也可以直接获取原始的模板字面量内容

 console.log(`\u00A9`) // ©

如果不想获取原始的内容,可以使用String.raw标签函数

 console.log(String.raw`\u00A9`) // \u00A9

Symbol

Symbol(符号)是 ECMAScript 6 新增的数据类型。是原始值,是唯一,不可变的,符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险

基本用法

需要使用 Symbol()函数初始化。因为符号本身是原始类型,所以 typeof 操作符对符号返回 symbol

const sym = Symbol();
console.log(typeof sym) // symbol

调用 Symbol()函数时,也可以传入一个字符串参数

const genericSymbol = Symbol();
const otherGenericSymbol = Symbol();

const fooSymbol = Symbol('foo');
const otherFooSymbol = Symbol('foo');
console.log(genericSymbol === otherGenericSymbol) // false
console.log(fooSymbol === otherFooSymbol) // false

Symbol()函数不能与 new 关键字一起作为构造函数使用。这样做是为了避免创建符 号包装对象

const mySymbol = new Symbol(); // Symbol is not a constructor

如果你确实想使用符号包装对象,可以借用 Object()函数

const myWrapperSymbol = Object(Symbol());
console.log(typeof myWrapperSymbol) // object

使用全局符号注册表

如果运行时的不同部分需要共享和重用符号实例,为此,需要使用 Symbol.for()方法

Symbol.for()对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运 行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同 字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例

let foo1 = Symbol.for('foo');
let foo2 = Symbol.for('foo');
console.log(foo1 === foo2) // true

采用相同的符号描述,在全局注册表中定义的符号跟使用 Symbol()定义的符号也并不等同

let foo3 = Symbol('foo');
console.log(foo1 === foo3) // false

可以使用 Symbol.keyFor()来查询全局注册表,这个方法接收符号,返回该全局符号对应的字 符串键。如果查询的不是全局符号,则返回 undefined

let for1 = Symbol.for('for1')
console.log(Symbol.keyFor(foo2)) // foo
console.log(Symbol.keyFor(for1)) // for1

如果传给 Symbol.keyFor()的不是符号,则该方法抛出 TypeError

Symbol.keyFor(123); // 123 is not a symbol

使用符号作为属性

凡是可以使用字符串或数值作为属性的地方,都可以使用符号,包括了对象字面量属性和 Object.defineProperty()/Object.defineProperties()定义的属性

let obj = {
  [Symbol.for('123')]: 123,
}
console.log(obj[Symbol.for('123')]) // 123
obj[Symbol.for('456')] = 456;
console.log(obj[Symbol.for('456')]); // 456
Object.defineProperty(obj, Symbol.for('789'), {
  value: 789,
})

console.log(obj[Symbol.for('789')]) // 789

Object.getOwnPropertyNames()返回对象实例的常规属性数组, Object.getOwnProperty- Symbols()返回对象实例的符号属性数组,这两个方法的返回值彼此互斥, Reflect.ownKeys()会返回两种类型 的键


console.log(Object.keys(obj)) // []
console.log(Object.getOwnPropertySymbols(obj)) // [Symbol(123), Symbol(456), Symbol(789)]
console.log(Reflect.ownKeys(obj)) // [Symbol(123), Symbol(456), Symbol(789)]
obj[Symbol('10')] = 10
console.log(Object.getOwnPropertySymbols(obj[Symbol('10')][3])) // 10

常用内置符号

ECMAScript 6 也引入了一批常用内置符号,用于暴露语言内部行为,开发者 可以直接访问、重写或模拟这些行为

💡: 在提到ECMAScript规范时,经常会引用符号在规范中的名称,前缀为@@。比如, @@iterator 指的就是 Symbol.iterator。

Symbol.asyncIterator

该方法返回对象默认的 AsyncIterator。 由 for-await-of 语句使用,换句话说,这个符号表示实现异步迭代器 API 的函数

class Foo {
  async *[Symbol.asyncIterator]() {
  }
}
const f = new Foo();
console.log(f[Symbol.asyncIterator]()) // AsyncGenerator {<suspended>}

class Emitter {
  constructor(max) {
    this.max = max;
    this.asyncIndex = 0
  }
  async *[Symbol.asyncIterator]() {
    while (this.asyncIndex < this.max) {
      yield new Promise((resolve, reject) => {
        resolve(this.asyncIndex++)
      })
    }
  }
}
async function test() {
  const emitter = new Emitter(5);
  for await(const x of emitter) {
    console.log(x); // 0 1 2 3 4
  }
}
test()

Symbol.hasInstance

该方法决定一个构造器对象是否认可一个对象是它的实例。由 instanceof 操作符使用

function Foo() { }
const f = new Foo();
console.log(f instanceof Foo) // true
console.log(Foo[Symbol.hasInstance](f)) // true
class Bar extends Foo {
  static [Symbol.hasInstance]() {
    return false;
  }
}
const b = new Bar();
console.log(b instanceof Bar) // false
console.log(Bar[Symbol.hasInstance](f)) // false

Symbol.isConcatSpreadable

这个符号作为一个属性表示“一个布尔值,如果是 true,则意味着对象应 该用 Array.prototype.concat()打平其数组元素”

ES6 中的 Array.prototype.concat()方法会 根据接收到的对象类型选择如何将一个类数组对象拼接成数组实例。覆盖 Symbol.isConcat- Spreadable 的值可以修改这个行为

 const arr1 = [1, 2, 3];
 const arr2 = [4, 5, 6];

 console.log(arr1.concat(arr2)) // [1, 2, 3, 4, 5, 6]

 arr2[Symbol.isConcatSpreadable] = false;
 console.log(arr1.concat(arr2)) // [1, 2, 3, [4, 5, 6]]

Symbol.iterator

这个符号作为一个属性表示“一个方法,该方法返回对象默认的迭代器。 由 for-of 语句使用”。换句话说,这个符号表示实现迭代器 API 的函数

class Foo {
  constructor(max) {
    this.max = max;
    this.idx = 0
  }
  *[Symbol.iterator]() {
    while (this.idx < this.max) {
      yield this.idx++
    }
  }
}
const f = new Foo(5);
for (const iterator of f) {
  console.log(iterator)  // 0 1 2 3 4
}

Symbol.match

这个符号作为一个属性表示“一个正则表达式方法,该方法用正则表达式 去匹配字符串。由 String.prototype.match()方法使用”

class SymbolMatch {
  static [Symbol.match](str) {
    return str.includes('foo')
  }
}
console.log('fooBar'.match(SymbolMatch)) // true
console.log('fBar'.match(SymbolMatch))// false

class SymbolMatch2 {
  constructor(str) {
    this.str = str;
  }
  [Symbol.match]() {
    return this.str.includes('foo')
  }
}
console.log('fooBar'.match(new SymbolMatch2('foo'))) // true

Symbol.replace

这个符号作为一个属性表示“一个正则表达式方法,该方法替换一个字符 13 串中匹配的子串。由 String.prototype.replace()方法使用”

class SymbolReplace {
  static [Symbol.replace](str) {
    return str.replace('foo', 'foo2')
  }
}
console.log('fooBar'.replace(SymbolReplace)) // foo2bar
console.log('fBar'.replace(SymbolReplace))// fBar

class SymbolReplace2 {
  constructor(str) {
    this.str = str;
  }
  [Symbol.replace]() {
    return this.str.replace('foo', 'foo2')
  }
}
console.log('fooBar'.replace(new SymbolReplace2('fooBar'))) // foo2bar

Symbol.search

这个符号作为一个属性表示“一个正则表达式方法,该方法返回字符串中 匹配正则表达式的索引。由 String.prototype.search()方法使用”

class SymbolSearch {
  static [Symbol.search](str) {
    return str.indexOf('foo')
  }
}
console.log('fooBar'.search(SymbolSearch)) // 0
console.log('fBar'.search(SymbolSearch))// -1
class SymbolSearch2 {
  constructor(str) {
    this.str = str;
  }
  [Symbol.search]() {
    return this.str.indexOf('foo')
  }
}
console.log('fooBar'.search(new SymbolSearch2('fooBar'))) // 0

Symbol.species

这个符号作为一个属性表示“一个函数值,该函数作为创建派生对象的构 造函数”。这个属性在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方 法。用 Symbol.species 定义静态的获取器(getter)方法,可以覆盖新创建实例的原型定义

class Bar extends Array { }
class Baz extends Array {
  static get [Symbol.species]() {
    return Array
  }
}
let bar = new Bar();
console.log(bar instanceof Bar) // true
bar = bar.concat(1);
console.log(bar instanceof Bar) // true
let baz = new Baz();
console.log(baz instanceof Baz) // true
baz = baz.concat(1);
console.log(baz instanceof Baz) // false

Symbol.split

这个符号作为一个属性表示“一个正则表达式方法,该方法在匹配正则表 达式的索引位置拆分字符串。由 String.prototype.split()方法使用”

class FooSplitter {
  static [Symbol.split](target) {
    return target.split('foo');
  }
}
console.log('barfoobaz'.split(FooSplitter)) // ['bar', 'baz']

class StringSpliter { 
  constructor(str) {
    this.str = str;
  }
  [Symbol.split](target) {
    return target.split(this.str)
  }
}
console.log('barfoobaz'.split(new StringSpliter('foo'))) // ['bar', 'baz']

Symbol.toPrimitive

这个符号作为一个属性表示“一个方法,该方法将对象转换为相应的原始 值。由 ToPrimitive 抽象操作使用”。很多内置操作都会尝试强制将对象转换为原始值,包括字符串、 数值和未指定的原始类型。对于一个自定义对象实例,通过在这个实例的 Symbol.toPrimitive 属性 上定义一个函数可以改变默认行为

class Bar extends Array { }
class Baz extends Array {
  static get [Symbol.species]() {
    return Array
  }
}
let bar = new Bar();
console.log(bar instanceof Bar) // true
bar = bar.concat(1);
console.log(bar instanceof Bar) // true
let baz = new Baz();
console.log(baz instanceof Baz) // true
baz = baz.concat(1);
console.log(baz instanceof Baz) // false
   */

    /*
class FooSplitter {
  static [Symbol.split](target) {
    return target.split('foo');
  }
}
console.log('barfoobaz'.split(FooSplitter)) // ['bar', 'baz']

class StringSpliter { 
  constructor(str) {
    this.str = str;
  }
  [Symbol.split](target) {
    return target.split(this.str)
  }
}
console.log('barfoobaz'.split(new StringSpliter('foo'))) // ['bar', 'baz']
     */

    /*
class Foo { }
let foo = new Foo();
console.log(3 + foo); // 3[object Object]
console.log(3 - foo); // NaN
console.log(String(foo)) // [object Object]
class Bar {
  constructor() {
    this[Symbol.toPrimitive] = function (hint) {
      switch (hint) {
        case 'number':
          return 3;
        case 'string':
          return 'string bar'
        default:
          return 'default bar'
      }
    }
  }
}
let bar = new Bar();
console.log(3 + bar); // 3default bar
console.log(3 - bar); // 0
console.log(String(bar)) // string bar

Symbol.toStringTag

这个符号作为一个属性表示“一个字符串,该字符串用于创建对象的默认 字符串描述。由内置方法 Object.prototype.toString()使用”

let set = new Set();
console.log(set.toString()) // [object Set]
console.log(set[Symbol.toStringTag]) // Set

class Foo { }
let foo = new Foo();
console.log(foo.toString()) // [object Object]
console.log(foo[Symbol.toStringTag]) // undefined

class Baz {
  [Symbol.toStringTag] = 'Baz'
}
let baz = new Baz();
console.log(baz.toString()) // [object Baz]
console.log(baz[Symbol.toStringTag]) // Baz

Symbol.unscopables

这个符号作为一个属性表示“一个对象,该对象所有的以及继承的属性, 都会从关联对象的 with 环境绑定中排除”

let o = {
  foo: 'bar',
  foo1: 'bar1',
}
with (o) {
  console.log(foo) // bar
  console.log(foo1) // bar1
}
o[Symbol.unscopables] = {
  foo1: true
}
with (o) {
  console.log(foo) // bar
  console.log(foo1) // foo1 is not defined
}

Object类型

对象其实就是一组数据和功能的合集, 可以通过new操作符创建

let obj = new Object();

如果没有参数,如 上面的例子所示,那么完全可以省略括号(不推荐)

let obj2 = new Object; // 可以,但是不推荐

ECMAScript 中的 Object 也是派生其他对象的基类。Object 类型的所有属性和方法在派生 的对象上同样存在

每个Object实例都有如下属性和方法

属性/方法描述
constructor用于创建当前对象的函数
hasOwnProperty用于判断当前对象实例(不是原型)上是否存在给定的属性
isPrototypeOf用于判断当前对象是否为另一个对象的原型
propertyIsEnumerable用于判断给定的属性是否可以使用
toLocaleString返回对象的字符串表示,该字符串反映对象所在的本地化执行环境
toString返回对象的字符串表示
valueOf返回对象对应的字符串,或者数值,或者布尔值表示,通常与toString()的返回值相同

因为在 ECMAScript 中 Object 是所有对象的基类,所以任何对象都有这些属性和方法