三刷红宝书之 JavaScript 的引用类型

前言

正如标题所说,这是我第三次翻开红宝书也就是《 JavaScript 高级程序设计第三版》,不得不说,虽然书有一些年份,不少知识点也不适合现代的前端开发,可是对于想要掌握 JavaScript 基础的前端新手,亦或是像我同样想找回曾经遗忘在记忆角落的那些碎片知识,这本书依旧很是的适合,不愧被成为 "JavaScript 圣经"javascript

本文是读书笔记,之因此又一次选择读这本书还有一个理由,以前都是记的纸质笔记,此次想把它做为电子版,也算是对以前知识的整理css

本文篇幅较长,目的是做为个人电子版学习笔记,我会尽量去其糟粕,取其精华,同时我会添加一些书上未记载但很重要的知识点补充html

上篇在这里前端

引用类型

引用类型是一种数据结构,它在别的语言中被称为类,可是在 JavaScript 中实际上并无类,虽然提供了不少相似“类”的语法(class 关键字),但不少书籍都认为这不是一个好的事情java

let date = new Date()
console.dir(date)
复制代码

这里经过 new 关键字来生成一个 Date 引用类型的实例,能够发现它的原型上有不少公有的方法,引用类型的实例也被成为引用类型的值,是一个对象node

ECMAScript 原生的引用类型有jquery

  • Object
  • Array
  • Date
  • RegExp
  • Function
  • Error

接下来我会介绍几个比较经常使用的引用类型来详细分析git

Object

Object 是最多见的引用类型,原生的引用类型都继承自 Object 类型,能够经过点表示法和方括号表示法来访问对象,前者书写更加简单,后者能够支持一些特殊语法和表达式,可是书写比较复杂(同时因为要解析表达式,性能也稍慢)github

let obj = {} // 经过字面量表示法来建立对象,比 new 更加简便

obj.a = 1
obj["b"] = 2
obj["c d"] = 3 // 方括号能够支持点表示法不支持的语法

// 方括号也能够支持表达式
// ps:对象中含有数字的属性会被转为字符串
obj[1 + 2] = 4
obj[obj] = 5 // 若是属性是一个对象,会转为原始类型
console.log(obj) // {"3": 4, "a": 1, "b": 2, "c d": 3, "[object Object]": 5}
复制代码

可使用 new 关键字来动态生成一个对象,另外它不只限于普通的对象,还能够生成包装类型的对象web

let str = "abc"
let wrappedStr = new Object("abc") // 等同于 new String("abc")

console.log(typeof str) // 'string'
console.log(typeof wrappedStr) // 'object'
console.log(wrappedStr instanceof String) // true 注意 String 首字母是大写,表明是 String 包装类型而非 string 基本类型
复制代码

wrappedStr 这个包装对象看上去和基本类型类似,但它是一个对象,进一步说是 String 包装类型的实例,这个咱们放到以后讲

Array

Array 类型能够经过 new 关键字调用或者字面量表达式来生成实例,当使用 new 并传入一个参数时,会建立参数长度的稀疏数组(由空单元组成的数组),当传入两个以上参数时,会建立由参数组成的数组

let arr1 = new Array(10)
let arr2 = new Array(10,20)

console.log(arr1) //[ <10 empty items> ]
console.log(arr2) // [ 10, 20 ]
复制代码

数组有一个 length 的内部属性,它是能够被修改的,能够利用这个特色快速清空数组和增长数组长度

let arr = [1,2,3] // 使用字面量建立数组

arr.length = 0
console.log(arr) // []

arr.length = 100
console.log(arr) // [ <100 empty items> ] 建立的都是空单元的稀疏数组,不推荐直接使用
复制代码

当尝试将 Array 类型转换为字符串时(这可能会存在于隐式转换中),默认会调用数组原型上的 toString 方法,它会依次调用数组中每一个元素的 toString 方法 (null 和 undefined 是例外,它们会直接转为空字符串)

let arr = [{a:1},123,()=>{},undefined]
console.log(arr.toString()) // "[object Object],123,()=>{}," 最后以逗号结尾,由于 undefined 变成空字符串了

let obj = {}
obj[arr] = ""
console.log(obj) // { '[object Object],123,()=>{},': "" } 当对象的属性是一个对象,JS 会将其转为字符串再做为其属性,这里就发生了隐式转换
复制代码

栈和队列

JavaScript 中的数组既能够表示栈也能够表示队列,当使用 pop 方法时会视为栈,将栈顶也就是数组最后一个元素弹出,当使用 shift 方法时会视为队列,将队首也就是数组第一个元素出列

而添加数组元素的方法 push,unshift 后会返回数组的长度

let arr = [1,2,3]

console.log(arr.push(4)) // 4 表示数组长度为4
console.log(arr.unshift(0)) // 5 表示数组长度为5
console.log(arr.pop()) // 4 返回最后一个元素4
console.log(arr.shift()) // 0 返回第一个元素0
复制代码

sort

经过调用 sort 方法能够给数组元素进行排序,可是这个方法有点特殊,sort 会调用每一个数组元素的 toString 方法,这会致使排序的结果和预期的有出入

let arr = [1,2,10,20,100,200]

console.log(arr.sort()) // [ 1, 10, 100, 2, 20, 200 ]
复制代码

调用 sort 会将每一个元素都转为字符串,最终会给 "1", "2", "10", "20""10", "20", "100", "200" 排序,而在上一章中我介绍到,JavaScript 中两个字符串比较,会逐个比较每一个字符的字符串的编码,若是当前字符编码相同,则依次比较下一位,一旦某个字符大于另外一个字符则直接返回结果,不会再日后比较

这里之因此 10 排在 2 的前面,是由于在比较 "10" 和 "2" 时,先比较第一位也就是字符串 1 和字符串 2,由于字符串 2 的编码大于字符串 1,因此就会直接退出比较,就会产生 "10" < "2" 的结果

这并非一个奇怪的 BUG,而是 sort 默认使用字典序进行排序,维基百科中是这么解释字典序的

设想一本英语字典里的单词,哪一个在前哪一个在后?

显然的作法是先按照第一个字母、以 a、b、c……z 的顺序排列;若是第一个字母同样,那么比较第二个、第三个乃至后面的字母。若是比到最后两个单词不同长(好比,sigh 和 sight),那么把短者排在前。

经过这种方法,咱们能够给原本不相关的单词强行规定出一个顺序。“单词”能够看做是“字母”的字符串,而把这一点推而广之就能够认为是给对应位置元素所属集合分别相同的各个有序多元组规定顺序:下面用形式化的语言说明。

若是不用默认的排序方式,能够经过给 sort 方法传入一个比较函数

let arr = [1,2,10,20,100,200]

console.log(arr.sort((a,b) => a - b)) // [ 1, 2, 10, 20, 100, 200 ]
复制代码

sort 是一个高阶函数,即支持传入一个函数做为参数,每次比较时都会调用传入的参数,其中参数 a 和 b 就是两个准备进行比较的字符串,在上一章还提到过,若是两个字符串相减会转为 Number 类型再进行计算,这样就能够避免字符串类型比较形成的问题

同时若是传入的函数返回值大于 0 ,则 b 会排在 a 前面,小于 0 则 a 会排在 b 前面,等于 0 则不变,根据这个特色能够控制返回数组是顺序仍是倒序

let arr = [1,2,10,20,100,200]

// [ 200, 100, 20, 10, 2, 1 ] 倒序数组
// 第一次比较时 b 为 2,a 为 1,因为返回值大于 0,因此 b 将排在 a 的前面,变成 [2,1],以此类推
// 注意是插入排序
console.log(arr.sort((a,b) => b - a))  
复制代码

sort 它会修改原数组,而不是新生成一个数组做为返回值,使用前请考虑清楚是否须要对原数组进行拷贝

let arr = [1,2,10,20,100,200]

console.log(arr.sort((a,b) => b - a)) // [ 200, 100, 20, 10, 2, 1 ]
console.log(arr) // [ 200, 100, 20, 10, 2, 1 ] 原数组被修改了!
复制代码

题外话:上述做为参数的函数中,能够经过 Math.random 随机返回大于小于 0 的数字能够实现数组乱序,可是并非真正的乱序,而洗牌算法能够解决这个问题

concat

concat 会将参数添加到数组末尾,不像 sort 会修改原数组,它会建立一个当前数组的副本(浅拷贝),因此相对比较安全

另外若是参数包含数组,会给数组进行一层降维

let arr = [1, 2, 3]

console.log(arr.concat(4, [5, 6], [7, [8, 9]])) // [ 1, 2, 3, 4, 5, 6, 7, [ 8, 9 ] ]
console.log(arr) // [1,2,3]
复制代码

固然如今更推荐使用 ES6 的扩展运算符,写法更加简洁,和 concat 实现的功能相似,一样也会浅拷贝数组

let arr = [1, 2, 3]

console.log([...arr, 4, ...[5, 6], ...[7, [8, 9]]]) // [ 1, 2, 3, 4, 5, 6, 7, [ 8, 9 ] ]
console.log(arr) // [1,2,3]
复制代码

slice

slice 会基于参数来切割数组,它一样会浅拷贝数组,当传入不一样参数会有不一样功能

  • 不传参数会直接返回一个浅拷贝后的数组
  • 只有第一个参数时,会返回第一个参数的下标到数组最后的数组,参数超过最大下标则返回空数组
  • 当传入两个参数时,会返回第一个参数至第二个参数 - 1 下标的数组
  • 若是第二个参数小于第一个参数(非负数),返回空数组
  • 参数含有负数则会加上数组长度再应用上述规则

slice 方法能够将类数组转为真正的数组

let arr = [1, 2, 3, 4, 5]

console.log(arr.slice()) // [1, 2, 3, 4, 5] 浅拷贝原数组
console.log(arr.slice(2)) // [3, 4, 5]
console.log(arr.slice(2, 3)) // [3]
console.log(arr.slice(2, 1)) // []
console.log(arr.slice(2, -1)) // [3, 4] 等同于 arr.slice(2, -1 + 5)
console.log(Array.prototype.slice.call({0: "a", length: 1})) // ["a"]
复制代码

splice

splice 能够认为是 push, pop, unshift, shift 的结合,而且可以指定插入/删除的位置,很是强大,但它传入但参数也更为复杂,因此通常只有在操做数组具体下标元素的时候才会使用,同时它也会修改原数组,使用时请注意

同时 splice 会返回一个数组,若是是使用它的删除功能,则返回的数组中会包含被删除的元素,来看一些比较特殊的例子

let arr = [1, 2, 3, 4, 5]

console.log(arr.splice(0, 1)) // [1]
console.log(arr) // [2, 3, 4, 5]
console.log(arr.splice(1)) // [3,4,5]
console.log(arr) // [2]
复制代码

第一次调用 splice 会在数组下标为 0 的位置删除一个元素,它的返回值就是被删除的元素 1,同时打印数组,会发现 splice 修改了原来的数组,原数组的第一个元素被删除了

第二次调用 splice 只传了一个参数,表示删除从数组下标为 1 的位置至数组最后一个元素,由于此时数组为 [2,3,4,5],因此删除下标从 1 到最后的元素 3,4,5,并做为 splice 的返回值,最后原数组就只包含元素 2 了

indexOf

indexOf 方法会返回参数在数组中的下标,不存在则返回 -1,一个特殊状况就是 NaN,若是使用 indexOf 判断 NaN 是否在数组中,永远会返回 -1

let arr = [1,2,3,4,5,NaN]

console.log(NaN) // -1
复制代码

解决这个问题可使用 ES6 的 includes 方法,它会返回一个布尔值,而非目标元素下标,同时它能够判断 NaN 是否存在与目标数组中

let arr = [1,2,3,4,5,NaN]

console.log(arr.indexOf(NaN)) // -1
console.log(arr.includes(NaN)) // true
复制代码

我我的习惯使用 includes 这个 api,由于平常中不少状况只须要知道参数是否存在于数组中,因此返回布尔值就足够了,若是遇到须要返回下标,或者从数组的指定位置搜索参数可使用 indexOf

reverse

reverse 和 sort 以及 splice 同样会修改原数组

let arr = [1,2,3,4,5]

console.log(arr.reverse()) // [5,4,3,2,1]
console.log(arr) // [5,4,3,2,1]
复制代码

迭代方法

ES5 为数组提供了 5 个迭代方法,它们都是高阶函数,第一个参数是一个函数,第二个参数是函数的 this 指向

  • every
  • filter
  • forEach
  • map
  • some

这些 api 平常用的很是多就不赘述了,只说一个小细节

对一个空数组不管参数中的函数返回什么,调用 some 都会返回 false, 调用 every 都会返回 true

let arr = []

console.log(arr.some(()=>{})) // false
console.log(arr.every(()=>{})) // true
复制代码

reduce

reduce 也就是归并方法,我的认为是数组中最高级的使用方法,用的好能够实现一些很是强大的功能,这里举个的例子:多维数组扁平化

const flat = function (depth = 1) {
    let arr = Array.prototype.slice.call(this)
    if(depth === 0 ) return arr
    return arr.reduce((pre, cur) => {
        if (Array.isArray(cur)) {
            // 须要用 call 绑定 this 值,不然会指向 window
            return [...pre, ...flat.call(cur,depth-1)]
        } else {
            return [...pre, cur]
        }
    }, [])
}
复制代码

关于 reduce 还有一个关于下标的注意点,当 reduce 只传一个参数时,index 的下标是从 1 也就是数组第二个元素开始的(若是此时数组为空会报错),当 reduce 传入第二个参数,会做为遍历的起始值,此时 index 的下标就从 0 也就是数组第一个元素开始

let arr = ["b", "c", "d", "e"]

arr.reduce((pre, cur, index) => {
    console.log(index)
    return pre + cur
})

// 1
// 2
// 3

arr.reduce((pre, cur, index) => {
    console.log(index)
    return pre + cur
}, "a")

// 0
// 1
// 2
// 3
复制代码

RegExp

正则表达式可使用构造函数动态生成,也可使用字面量快速生成,因为正则表达式实例也是对象,因此也会有属性,经常使用的属性有

  • global: 是否设置了 g 标志,即开启全局匹配
  • ignoreCase: 是否设置了 i 标志,即忽略大小写
  • dotAll (ES9) : 是否设置了 s (并不是 d )标志,即便用 . 能够匹配任何单个字符,能够理解为 [\s\S](默认 . 不会匹配换行符,回车符,分隔符等)
  • lastIndex: 开始搜索下一个匹配项的字符位置,从 0 算起
  • source: 当前正则表达式的字符串表示
let reg = /\[abc]/ig
console.dir(reg)

let reg2 = new RegExp('\\[abc]', "ig") // RegExp 第二个参数为正则标志
console.dir(reg2)

console.log(reg.test("[abc]"), reg2.test("[abc]"))
复制代码

打印结果以下

reg 和 reg2 实现的功能是相同的,第一种更简便,第二种更灵活,须要结合实际状况灵活使用

另外还有一个比较重要的点,能够看到 2 个正则对象的 lastIndex 都为 5,此时若是继续用 test 方法匹配会返回 false

let reg = /\[abc]/ig
console.dir(reg)

let reg2 = new RegExp('\\[abc]', "ig") // RegExp 第二个参数为正则标志
console.dir(reg2)

console.log(reg.test("[abc]"), reg2.test("[abc]"))
console.log(reg.test("[abc]"), reg2.test("[abc]"))

复制代码

之因此出现这样的状况是由于正则对象内部保存的 lastIndex 属性会决定下次正则匹配的位置,第一次用 test 方法匹配成功后,lastIndex 从 0 变成 5,同时下次会从参数的第 6 个元素开始匹配,此时发现匹配不到任何元素,因此会返回 false ,并将 lastIndex 重置为默认值 0

关于正则其实很是复杂,深刻会涉及到状态机,回溯,贪婪匹配等知识点,写的很差会影响系统的性能,可是若是能搞懂其中的奥秘,写出优质的正则,可以很大程度上解放劳动力,例如给整个项目替换部分代码,另外 Vue 的模版字符串编译也是依赖正则的匹配来提取属性,自定义指令等

Function

函数也是对象,而函数的函数名只是做为一个指向函数的指针,这个我在上一章也提到过,JavaScript 不容许直接操做对象的内存空间,开发中操做的都是指向堆内存对象的指针

能够经过 new 关键字来动态生成一个函数

let func = new Function("a","console.log(a)")

console.log(func(123)) // 123
复制代码

Function 函数做为构造函数时,至少接受一个参数,当只传入一个参数时,会直接将参数做为函数体,当传入超过一个函数时,会将最后一个参数做为函数体,以前的全部参数会做为生成的函数的参数

经过 new 动态生成函数优势在于更加灵活,例如 Vue 的编译器最终会将字符串经过 new Function 传入 code 代码并执行

// src/compiler/to-function.js:11
function createFunction (code, errors) {
  try {
    return new Function(code)
  } catch (err) {
    errors.push({ err, code })
    return noop
  }
}
复制代码

而通常状况咱们是不须要使用这种方式的,直接使用字面量的形式建立函数,由于前者虽然更加灵活,可是性能并非很是理想,同时还有可能存在安全隐患(相似 eval,可能会被恶意用户注入恶意代码运行,造成 XSS 攻击)

重载

JavaScript 中的函数没有重载,可是能够实现必定程度上的参数重载

import $ from 'jquery'

$("p").css("background-color"); // color
$("p").css("background-color",'red');
复制代码

这里是一个 jquery 的代码,当调用 css 方法传入一个参数时,会返回 p 节点当前的背景颜色,若是传入 2 个参数,会设置背景颜色,css 方法的功能取决于传入参数的个数,原理是利用函数参数个数来判断返回属性仍是设置属性

函数声明,函数表达式

当 JavaScript 在加载数据时(也能够理解为进入一个新的环境或者说上下文),会优先读取环境中的全部函数声明,而且这是在加载代码以前执行的操做,而当全部准备工做完成后,才会逐行执行代码,也就是说当代码执行到函数表达式时,函数才会被真正执行

func()

// 函数表达式
const func2 = function () {
    console.log(2)
}

func2()

// 函数声明
function func() {
    console.log(1)
}
复制代码

执行顺序:func 声明 -> func 执行 -> func2 声明 -> func2 执行

在进入全局环境以前,func 因为"函数声明提高"被提高到最前面执行,随后再是执行整个代码,因此上面代码并不会报错,fun2 是函数表达式,若是将 func2 执行的代码放到 func2 声明以前,就会发生错误,由于函数表达式不会被提高

关于函数声明和函数表达式还有一些小细节:

  • 若是在全局环境中使用函数声明的形式建立函数,那么它会被看成全局函数
  • 函数表达式能够理解为建立一个匿名函数,而后将匿名函数赋值给声明的变量
  • 能够同时使用函数声明和函数表达式
var sum = function sum() {
    //...
    sum() // 在函数内部 sum 指向的是右边的词法名称,而非左边的 sum 变量
}
复制代码

右边的 sum 会做为匿名函数的“词法名称”,这种状况经常使用于自身的递归,若是没有这个词法名称函数只能使用 arguments.callee 方法来实现递归(没法使用左边的 sum 变量),而函数内部的 arguments 对象因为性能问题已不推荐使用,因此若是有递归的需求,推荐给匿名函数添加一个词法名称,另外须要注意的是,词法名称是常量,函数内部没法修改

每日一题中就有该知识点的考察,如下是某个解题者的解释

var b = 10;
(function b() {
   // 内部做用域,会先去查找是有已有变量b的声明,有就直接赋值20,确实有了呀。发现了具名函数 function b(){},拿此b作赋值;
   // IIFE的函数没法进行赋值(内部机制,相似const定义的常量),因此无效。
  // (这里说的“内部机制”,想搞清楚,须要去查阅一些资料,弄明白IIFE在JS引擎的工做方式,堆栈存储IIFE的方式等)
    b = 20;
    console.log(b); // [Function b]
    console.log(window.b); // 10,不是20
})();
复制代码

length

函数还有一个 length 属性,它表示的是函数但愿接受的形参个数

形参的数量不包括剩余参数个数,仅包括第一个具备默认值以前的参数个数

function func(a,b,...c) {}

console.log(func.length) // 2

function func2(a = 1,b,c) {}

console.log(func2.length) // 0

function func2(a,b = 2,c) {}

console.log(func2.length) // 1
复制代码

能够看到第一个函数的 length 属性为 2,由于后面是剩余参数,不计算在 length 长度中,而第二个由于参数 a 含有默认值,因此会返回 a 以前的参数,同时由于 a 以前没有参数因此最终返回 0,第三个例子中参数 b 有默认值,因此返回 b 以前的参数个数,也就是 1

apply / call

函数在运行时还会生成一个 this 对象,它能够理解为指针,在通常状况下,this 指向的是调用该函数的对象,而使用 函数 apply / call 方法能够改变 this 的指向(再次强调,由于函数也是对象,因此也会有属性和方法)

function func(a,b,c) {
    console.log(a,b,c)
    console.log(this)
}

func.call({a:1},1,2,3) // 1,2,3 {a:1}
func.call('123',1,2,3) // String{"123"} {a:1}
func.apply({a:1},[1,2,3]) // 1,2,3 {a:1}
复制代码

apply 和 call 的区别在于 apply 的第一个参数为即将执行的函数的 this 值,第二个参数为数组或者类数组,表明即将执行的函数的参数,而 call 第一个参数相同,第二个至最后的参数表明即将执行的参数

即 apply 会用数组保存函数的参数,call 则会平铺,除此之外没有区别(虽然 call 方法必需要将函数参数平铺,可是可使用 ES6 的扩展运算符将其写为数组的形式,如今更推荐使用 call )

另外 apply 和 call 都会让第一个参数进行装箱操做,即若是传入一个基本类型且基本类型有包装类型(下文会详细解释包装类型),则 this 的值为传入的基本类型的包装类型,例子中将 string 类型变成了 String 的包装类型

非严格模式下,若是 this 的值为 null / undefined,则自动会指向全局的 window 对象,而严格模式则不会有这个行为(值得注意的是这个并不是 apply / call 的行为)

function func() {
    console.log(this)
}

func.call(undefined) // window 对象


function func2() {
 "use strict"
    console.log(this)
}

func2.call(undefined) // undefined
复制代码

bind

bind 和 apply / call 方法相似,也是一个用来改变函数 this 指向的方法,区别在于 bind 会返回一个被绑定 this 指向函数,而 apply / call 则直接会运行它,若是须要绑定 this 指向,又不想当即执行的话,可使用 bind 方法,等须要使用时再调用绑定后的函数

bind 第一个参数为 this 指向,第二个至之后的参数为给绑定的函数预先传入的参数,预置参数的函数一般也被称为偏函数

function func(a,b,c,d) {
    console.log(this)
    console.log(a,b,c,d)
}

let boundFunc = func.bind({a:1},1,2)

boundFunc(3,4) // {a:1} 1,2,3,4
复制代码

经过 bind 预先传入了参数 1,2,当调用绑定后的函数时,它会预先传入 1,2 做为第一第二个参数,此时再给函数传入参数,会做为第三第四的参数,最终打印 1,2,3,4

基本包装类型

为了便于操做基本类型的值,ECMAScript 提供了 3 个特殊的引用类型:

  • Boolean
  • Number
  • String

它们有别于 boolean,number,string ,能够发现它们首字母是大写,意味着它们是引用类型,也就是对象

JS 高级程序设计中说到:

每当读取一个基本类型值的时候,后台就会建立一个对应的基本包装类型对象,从而让咱们可以调用一些方法来操做这些数据

var s1 = "some text"
var s2 = s1.substring(2)
复制代码

咱们要知道,基本类型是没有任何方法的,也就是说 "some text" 这个字符串是没有 substring 这个方法的,那为何第二行代码不会报错呢?

缘由在于,当第二行代码访问保存着字符串的变量 s1 时,会处于一种读取模式,尝试从内存中读取这个字符串的值,而在读取模式中访问字符串时,会有如下操做

  1. 建立 String 类型的一个实例
  2. 在实例上调用 substring 方法
  3. 还原成基本最初的基本类型

能够这样理解

var s1 = "some text"
// 读取模式
s1 = new String("some text")
var s2 = s1.substring(2)
s1 = "some text" // 还原成基本类型
复制代码

String 包装类型做为引用类型,它是含有 substring 方法的,因此须要将基本类型转换为包装类型才能执行方法,并将返回的结果赋值给变量 s2,最后再将 s1 还原成一开始的基本类型

另外自动建立 (并不是主动调用 new 建立的对象) 的包装类型只会存在于代码执行瞬间,而后当即销毁

直接调用 String 函数生成的是基本类型,而使用 new 关键字将 String 做为构造函数使用,则生成的是包装类型

let str = String("abc") // "abc" string 基本类型
let wrappedStr = new String("abc") // String {"abc"} String 包装类型

let boolean = Boolean(true) // true boolean 基本类型
let wrappedBoolean = new Boolean(true) // Boolean {true} Boolean 包装类型

let number = Number(123) // 123 number 基本类型
let wrappedNumber = new Number(123) // Number {123} Number 包装类型
复制代码

通常状况下不推荐主动生成包装类型,由于容易和基本类型搞混

Number 包装类型

toFixed 是 Number 包装类型下的一个方法,用于按照指定小数位返回数值的字符串表示,当数值小于传入当参数,会四舍五入

let num = 10
console.log(num.toFixed(2)) // "10.00"

let num2 = 1.68
console.log(num.toFixed(1)) // "1.7"
复制代码

可是使用 toFixed 时须要考虑到 Javascript 的小数的精度问题

let num = 1.335
console.log(num.toFixed(2)) // "1.33"
console.log(num.toFixed(50)) // "1.3349999999999999644..."
复制代码

事实上数字 1.335 在语言底层并非真的以 1.335 来存储的,经过 toFixed 方法返回小数点后 50 位能够发现,1.335 真正的值为 1.3349999... 以后就是无限的循环,因为 JS 最多能表示的精度的长度是 16,全部的小数都只会精确到小数点后 16 位同时自动凑整,因此就进位以后就获得了 1.335

因为表明的真实数字是 1.3349999...,因此 toFixed 四舍五入后的结果也就是 1.33 了,由于下一位是 4 被舍去了

String 包装类型

slice

slice 方法同时可使用于数组类型和 String 包装类型,具体的特色能够看上面的 Array 章节

indexOf

indexOf 方法同时可使用于数组类型和 String 包装类型,它会从第一个位置开始遍历,寻找参数在字符串(数组)中的位置并返回下标,同时它还接受第二个参数用于在指定的位置以后开始寻找

let str = "hello world"
// 从下标 6 的位置开始寻找字符串 o
// 但返回值仍相对于整个字符串的位置
console.log(str.indexOf('o',6)) // 7
复制代码

尽管 ES6 有 includes 和 find 之类更强大的方法去寻找元素,可是若是须要从某个指定位置开始寻找元素,使用 indexOf 会更加的方便,如下例子会返回字符串中单词 e 的全部下标

let str = ` React makes it painless to create interactive UIs. Design simple views for each state in your application, and React will efficiently update and render just the right components when your data changes `
let arr = []
let pos = str.indexOf('e')
while (pos > -1) {
    arr.push(pos)
    pos = str.indexOf('e',pos + 1)
}
console.log(arr) // [6,14,25,34,37,42,49,62,73,77,85,94,122,132,138,149,156,159,169,183,190,208]
复制代码

trim

trim 方法能够去除字符串首尾的空格,对于一些表单的输入是不容许有空格的,可使用 trim 来去除,同时在 ES10 中,还有 trimStart 和 trimEnd 这两种方法,分别去除字符串前面和后面的空格

replace

replace 方法能够用来根据参数替换字符串,第一个参数能够是字符串也能够是正则,第二个参数能够是字符串也能够是函数

str.replace(regexp|substr, newSubStr|function)

当参数都是字符串时,只是简单的在 str 中找到第一个参数第一次出现的位置,并替换成第二个参数

当第一个参数是正则时,会替换和正则匹配的字符串,同时若是正则中含有 g 标志,会进行全局搜索,当第一次匹配到对应字符串后,再也不中止匹配,而是继续日后搜索是否仍有可替换的字符串

同时第二个参数还能够传入函数,匹配到的字符串会替换为函数的返回值,函数的引入使得 replace 方法更加的灵活

let str = "hello world"
console.log(str.replace('l','x')) // "hexlo world" 只将第一个 l 替换为 x
console.log(str.replace(/l/,'x')) // "hexlo world" 同上
console.log(str.replace(/l/g,'x')) // "hexxo worxd" 经过给正则添加全局搜索的标志符,能够实现全局替换
console.log(str.replace(/l/, (match,index,str) => {
    console.log(match) // 'l' 匹配的子串
    console.log(index) // 2 匹配到的子字符串在原字符串中的偏移量
    console.log(str) // 'hello world' 被匹配的原字符串
    return 'q' // 返回字符串 q 表明将第一个匹配到的字符串 l 替换为 q
})) // "heqlo world"
复制代码

另外若是第一个正则含有捕获组,那么第二个参数还能够拿到前面正则中的捕获组,更多详情能够查看 MDN

split

关于 split 方法用来分割字符串,除了咱们经常使用的传入一个字符串外,其实 split 还支持传入一个正则做为参数,若是是一个包含捕获组的正则表达式,会将捕获组也放入最终返回的数组中

let str = "hello world"
console.log(str.split(/(l)/)) // [ 'he', 'l', '', 'l', 'o wor', 'l', 'd' ]
复制代码

能够看到这里不只经过分隔符 'l' 将字符串分割,还将分隔符也保存在了数组中

单体内置对象

事实上,并无所谓的全局变量或全局函数,全部在全局做用域中定义的属性和函数,都是 Global 对象的属性,包括原生的引用类型都是 Global 对象的属性, Global 对象通常不容许被直接访问

之因此是 Global 对象而不是 window 对象,是由于 window 对象只在浏览器中存在,在 node 环境下,全局对象为 global,而在 webworker 中全局对象为 self,它们虽然都不是 Global 对象,可是都做为承担它的对象而存在

// 在 node 环境运行如下代码
console.log(global) // node 中的全局对象 global
console.log(window) // 报错 window 对象不存在

// 在 webworker 中不容许操做 DOM,也没有 window 对象
复制代码

本系列第一篇中我提到过,在浏览器端,JavaScript 有三部分组成

  • ECMAScript
  • DOM
  • BOM

也就是说有一部分的方法并不属于 ECMAScript 的范畴,好比 BOM 提供的 alert,console 方法,或者 DOM 提供的 createElement,appendChild,换句话说,不仅是 JavaScript,别的语言也能操做 DOM ,操做控制台等,浏览器只提供了访问它们的接口

JavaScript Weekly 这周正好给我推荐了一篇不错的文章,介绍 ECMAScript 最新的提案 globalThis,它存在与全部 JavaScript 运行的平台,并指向全局的 Global 对象

Explaining the globalThis ES Proposal

未完待续

参考资料

《JavaScript 高级程序设计第三版》

《你不知道的JavaScript》

MDN

JavaScript 浮点数陷阱及解法

相关文章
相关标签/搜索