第五章 引用类型

Object类型和Array类型

创建方式

对象和数组都有两种创建方式:

  • 使用 new 操作符后跟 Object/Array 构造函数
  • 对象字面量表示法

Array类型

1. 检测数组

前面我们提到使用 instanceof 操作符可以检测引用类型,(参考:检测基本类型和引用类型值)

instanceof 操作符的问题在于,它假定只有一个全局执行环境。如果网页中包含多个框架,那实 际上就存在两个以上不同的全局执行环境,从而存在两个以上不同版本的 Array 构造函数。如果你从 一个框架向另一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有各自 不同的构造函数。

为了解决这个问题,ES5新增了Array.isArray() 方法,判断是否是数组,如果是则返回true

const arr = []
Array.isArray(arr) // true
1
2

2. 转换方法

TODO

后续学习补充

3. 数组的栈方法

ES的数组提供了让数组的行为类似与栈的数据结构的方法,栈是后进先出的数据结构,只能操作栈顶

  • pop()方法,从数组末尾移除后一项
  • push()方法,可以接收任意数量的参数,把它们逐个添加到数组末尾

4. 数组的队列方法

ES的数组还提供了可以实现类似队列数据结构的方法

结合使用 shift()和 push()方法,可以像使 用队列一样使用数组

同时使用 unshift()和 pop()方法,可以 从相反的方向来模拟队列,即在数组的前端添加项,从数组末端移除项

  • shift(),移除数组中的第一项并返回该项
  • unshift(),在数组前端添加任意个项并返回新数组的长度

5. 排序

  • reverse(), 反转数组顺序
  • sort(), 排序,根据字符串比较,所以对于其他数据类型的排序兼容不好

Number数据类型使用sort()方法示例:

[1, 2, 13, 14, 24].sort() // [1, 13, 14, 2, 24]
1

为了解决sort方法的不足,我们可以给它传一个比较函数作为参数,如下:

// 通过返回大于0,小于0和等于0的值来影响排序结果
function compare(val1, val2) {
  return val2 -val1
}
var arr = [1, 13, 14, 2, 24]
arr.sort(compare) // [24, 14, 13, 2, 1]
1
2
3
4
5
6

6. 操作方法

  • concat(), 复制当前数组,还可以通过单个参数或者数组作为参数,拼接到数组末尾
var arr = ['red', 2]
arr.concat() // ["red", 2]
arr.concat(7, ['blue', 9]) // ["red", 2, 7, "blue", 9]
1
2
3
  • slice(), 根据参数截取原始数组返回一个新数组

一个参数,从当前位置截取至末尾
两个参数,截取参数范围内的数组, 注意返回值不包含第二个参数的位置

注意

如果slice()的参数是负数,则用数组长度加上该数来确定相应位置
如果结束位置小于起始位置,则返回空数组

var arr = ["red", "green", "yellow", "gray"]

// 传入一个参数
arr.slice(1) // ["green", "yellow", "gray"]
// 两个参数,
arr.slice(1, 3) // ["green", "yellow"]
// 负数参数
arr.slice(-3, -1) // ["green", "yellow"]
arr.slice(-6, -5) // []
1
2
3
4
5
6
7
8
9
  • splice(), 删除、插入、替换数组,返回删除的项,若没有删除的项则返回空数组

删除,传入两个参数,起始位置和删除的项数
插入,至少传入三个参数,起始位置、删除的项数(0)和插入的项(可以传入多个) 替换,至少三个参数,起始位置、删除的项数和插入的项(可以传入多个)

var arr = ["red", "green", "yellow", "gray"]
// 删除
arr.splice(0, 1) // ["red"]
// 插入
arr.splice(0, 0, "blue", "pink") // [] 因为没有删除的项
console.log(arr) // ["blue", "pink", "green", "yellow", "gray"]
// 替换
arr.splice(0, 1, "blue", "pink") // ["red"]
console.log(arr) // ["blue", "pink", "green", "yellow", "gray"]
1
2
3
4
5
6
7
8
9

7. 位置方法

  • indexOf(), 从初始位置查找,返回查找项的索引,找不到则返回-1
  • lastIndexOf(), 从末尾位置查找,返回查找项的索引,找不到则返回-1

上面两个方法都接收两个参数:

  • 第一个参数,必填,要查找的项
  • 第二个参数, 非必填,查找的起始位置

注意: 这两个方法查找会使用全等操作符,所以查找的项必须严格相等

var arr = ["red", "green", "yellow", "green", "gray"]
arr.indexOf("green") // 1
arr.indexOf("green", 2) // 3

arr.lastIndexOf("green") // 3
arr.lastIndexOf("green", 2) // 1

var obj = {name: 'fmy'}
var arr = [{name: 'fmy'}]
arr.lastIndexOf(obj) // -1
arr = [obj]
arr.lastIndexOf(obj) // 0
1
2
3
4
5
6
7
8
9
10
11
12

8. 迭代方法

下面共五个迭代方法,可接收三个参数

  • 第一个参数,必填,数组每项的值
  • 第二个参数,非必填,该项在数组中的位置
  • 第三个参数,非必填,数组对象本身
  • every(), 查询数组中的每一项是否满足条件,若有一项不满足返回false,全都满足返回true
var num = [1, 2, 3, 4, 5, 6]
num.every(item => item < 7) // true
num.every(item => item < 5) // false
1
2
3
  • some, 查询数组中的是否有满足条件的项,若有至少一项满足返回true,全都不满足返回false
var num = [1, 2, 3, 4, 5, 6]
num.some(item => item < 4) // true
num.every(item => item < 0) // false
1
2
3
  • filter(), 返回满足条件的项组成的数组
var num = [1, 2, 3, 4, 5, 6]
num.filter(item => item < 4) // [1, 2, 3]
num.filter(item => item < 0) // []
1
2
3
  • forEach(), 没有返回值,对数组的每一项执行某些操作,类似for循环
var num = [1, 2, 3, 4, 5, 6]
num.forEach(item => {
  console.log(item + 1) // 2 3 4 5 6 7
})
1
2
3
4
  • map(), 返回操作后的数组
var num = [1, 2, 3, 4, 5, 6]
var afterNum = num.map(item => {
  return item*2
})
console.log(afterNum) // [2, 4, 6, 8, 10, 12]
1
2
3
4
5

9. 归并方法

  • reduce(), 从数组初始位置操作
  • reduceRight(), 从数组末尾操作

以上两个方法接收四个参数:

  • 第一个参数,必填, 前一个值
  • 第二个参数,非必填, 当前值
  • 第三个参数,非必填,项的索引
  • 第四个参数,非必填,数组对象

可以用于数组中所有值的求和操作

// reduce
var num = [1, 2, 3, 4, 5]
var sum = num.reduce((pre, cur, index) => {
  return pre + cur
})
console.log(sum) // 15

// reduceRight
var num = [1, 2, 3, 4, 5]
var sum = num.reduceRight((pre, cur, index) => {
  return pre + cur
})
console.log(sum) // 15
1
2
3
4
5
6
7
8
9
10
11
12
13

Date类型和RegExp类型

TODO

待补充

Function类型

函数实际上是对象,每个函数都是Function类型的实例,而且都与其他引用类型一样具有属性和方法。
基于函数是对象,所以函数名实际上也是一个指向函数对象的指针

没有重载

重载的概念

方法的重载是指一个类中可以定义有相同的名字,但参数不同的多个方法。调用时,会根据不同的参数表选择对应的方法

js中没有重载的概念,上面提到函数名实际是一个指针,基于这个概念,更容易理解js为什么没有重载

我们看一个示例:

function add(num) {
  return num + 1
}

function add(num, num1) {
  return num + num1 + 1
}

var result_1 = add(10) // NaN
var result_2 = add(10, 20) // 31
1
2
3
4
5
6
7
8
9
10

为了更便于理解js为什么没有重载,上面的实例实际等价于:

var add = function (num) {
  return num + 1
}
add = function (num, num1) {
  return num + num1 + 1
}

var result_1 = add(10) // NaN
1
2
3
4
5
6
7
8

通过第二个示例,我们就知道,再创建第二个函数时,实际上覆盖了引用第一个函数的变量add

函数声明与函数表达式的区别

知识点

除了什么时候可以通过变量访问函数这一点区别之外,函数声明与函数表达式的语法其实是等价的

解析器在向执行环节加载数据时,对函数声明和函数表达式并非一视同仁。
解析器会率先读取函数声明,并使其在执行任何代码之前可用;
而对于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被执行

为了理解上面的知识点,我们看一个示例:

// 函数声明
console.log(add(10, 20)) // 30
function add(num1, num2) {
  return num1 + num2
}

// 函数表达式
console.log(add(10, 20)) // add is not a function
var add = function (num1, num2) {
  return num1 + num2
}
1
2
3
4
5
6
7
8
9
10
11

函数内部的属性

函数内部有两个特殊的对象:arguments 和 this

arguments

js的函数不介意传递进来多少个参数,也不介意参数是什么类型
之所以这样,是因为在函数内部,参数是用一个数组(arguments)表示

arguments 是一个类数组对象,包含着传入函数中的所有参数
arguments 对象只是与数组类似,并不是Array的实例。可以使用方括号语法访问它的元素,使用length属性来确定传进来多少个参数

我们看一个示例:

function sum() {
  return arguments[0] + arguments[1]
}
sum('你好', '世界') // "你好世界"
1
2
3
4

arguments的主要用途是保存函数参数,除此外,arguments 对象还有一个属性 callee,该属性是一个指针,指向拥有 arguments 对象的这个函数

我们看一个经典阶乘函数:

function factorial(num) {
  if (num <= 1) {
    return 1
  } else {
    return num * factorial(num - 1)
  }
}
1
2
3
4
5
6
7

上面的阶乘函数用到了递归算法,但是存在函数的执行与函数名耦合在一起的问题,为了消除这种耦合,可以使用arguments.callee

function factorial(num) {
  if (num <= 1) {
    return 1
  } else {
    return num * arguments.callee(num - 1)
  }
}
1
2
3
4
5
6
7

this

this引用的是函数据以执行的环境对象

看一个示例:

window.color = 'red'
var obj = {
  color: 'green'
}

function outputColor() {
  console.log(this.color)
}

outputColor() // 'red'
obj.outputColor = outputColor
obj.outputColor() // 'green'
1
2
3
4
5
6
7
8
9
10
11
12

函数的属性和方法

我们现在知道函数是对象,函数也有属性和方法

知识点

每个函数都包含两个属性: length 和 prototype

length属性表示函数希望接收的命名参数个数, 如下:

function foo(num) {
  return num
}
foo.length // 1
1
2
3
4

prototype 保存了所有的实例方法,prototype方法通过各自对象的实例访问,这个属性我们在后面章节详解

知识点

每个函数都包含两个非继承而来的方法:apply() 和 call()

apply() 和 call() 都用来设置函数体内this对象的值, 两者的区别仅在于接收参数的方式不同

apply() 接收两个参数:

  • 第一个是运行函数的作用域
  • 第二个是参数数组(可以是 Array 的实例也可以是 arguments 对象)
function sum(n1, n2) {
  return n1 + n2
}

function callSum(n1, n2) {
  return sum.apply(this, [n1, n2])
}

callSum(1, 3) // 4
1
2
3
4
5
6
7
8
9

call() 与apply()方法类似:

  • 第一个是运行函数的作用域
  • 其余参数直接传递给函数
function sum(n1, n2) {
  return n1 + n2
}

function callSum(n1, n2) {
  return sum.call(this, n1, n2)
}

callSum(1, 3) // 4
1
2
3
4
5
6
7
8
9

apply() 和 call() 能够改变运行作用域

window.color = 'red'
var obj = {
  color: 'blue'
}

function sayColor () {
  console.log(this.color)
}

sayColor() // red
sayColor.call(0) // blue
1
2
3
4
5
6
7
8
9
10
11

基本包装类型

ECMAScript 提供了 3 个特殊的引用类型:Boolean、Number 和 String
这三个类型与其他引用类型相似,具有各自特殊的方法

我们知道基本类型值不是对象,从逻辑上讲不应该有方法
为了实现基本类型的直观的操作,后台自动完成一系列的处理

我们看一个例子:

var str = 'hello'
var str2 = str.substring(1) // 'ello'
1
2

在上面例子中,后台自动完成了下列处理:

  1. 创建String类型的一个实例
var str = new String('hello')
1
  1. 在实例上调用方法
var str2 = str.substring(1)
1
  1. 销毁实例
str = null
1

Boolean和Number类型对应的布尔值和数字 也适用于上述步骤

引用类型与基本包装类型的区别

主要区别是对象的生存期不同

  1. 引用类型的实例,在执行流离开当前作用域之前一直保存在内存中
  2. 自动创建的基本包装类型的对象,则只存在于一行代码执行的瞬间,然后立刻销毁

这意味不能在运行时为基本类型值添加属性和方法