引用类型之「对象/数组」

# 引用类型种类

JS中引用类型有:
(1) 对象:Object
(2)数组:Array
(3)日期:Date
(4)正则:RegExp
(5)函数:Function
(6)基本包装类型:String、Number、Boolean
(7)单体内置对象:Global、Math

# 概念澄清

Object引用类型,简称Object类型(或对象类型),是引用类型中最使用最广泛的一种数据结构,常说的对象,是Object类型的实例,使用new关键字后面跟构造函数 Object来创建,构造函数本身只是一个函数,只不过该函数是出于创建新对象的目的而定义的。
  判断一个实例的数据类型是否是引用类型,使用typeof方法,判断结果是否是字符串object;而判断一个实例的数据类型是哪一种引用类型,可以使用constructor 获取构造函数,或者使用instanceof操作符判断构造函数的原型对象是否在实例的原型链上。
  阅读 构造函数与原型(链)

# Object 类型

  Object类型是ECMAScript中使用最多的数据类型,对象是Object类型的实例,主要应用在应用程序中存储和传输数据中,用来封装参数传递。

创建Object实例
(1)new操作符 + 构造函数:var person = new Object(); person.name = 'zhangfs'
(2)对象字面量表示法:var person = { name: 'zhangfs' } 属于(1)的简写形式。但它不会调用Object构造函数。

访问属性
(1)点表示法:var userName = person.name 最常用。
(2)方括号表示法:var userName = person['name'] 这种方式可支持变量访问属性,可以访问数值属性,及属性中包含空格等特殊情况。
【注】如果对象中不存在该值,不会报错,而是返回 undefined

删除属性
(1)使用delete关键字:delete person.name
【注】如果只是想清空属性,使用person.name = ''即可

复制(拷贝)
(1)浅拷贝:其原理是将源对象的每一个属性都做一个拷贝,并不考虑属性的类型是否为引用类型。扩展一些理解,其只是做了地址引用的复制,并没有得到真正的值。经过浅拷贝的变量修改属性仍有可能对源对象造成影响

function shallowCopy(obj) {
  let c = {}
  for (let x of obj) {
    c[i] = x
  }
  return c
}
  • 直接赋值浅拷贝:var obj= new Object(), var copy = obj
  • Object.assign()浅拷贝。该方法主要目的是合并多个对象,实现数组concat()效果。接收大于等于2个参数,从第二个参数开始将对象属性合入第一个对象中,靠后的对象属性值覆盖前面的对象属性值。方法返回合并后的对象。建议将第一个参数设定为空对象 {}。此处讲其浅拷贝用法:
# Object.assign()实现的浅拷贝
var obj = {a: 1, b: { b1: 2 }}
var o = Object.assign({}, obj)
o.a = 3
o.b.b1 = 4
console.log(obj)
// --- output ---
// { a: 1, b: { b1: 3 } }

(2)深拷贝
  顾名思义:就是对任意层次的属性值均做拷贝,拷贝后数据不再互相干扰
1)JSON序列化深拷贝:JSON.parse(JSON.stringify(obj))
【注】这种深拷贝方法缺陷在于:JSON.stringify()会主动丢弃值为undefinedFunctionSymbol原型属性。所以parse()后的值可能与源数据有缺失。
2)递归式深拷贝
  简化版的递归式深拷贝,重点关注对象属性的attr.constructor === Array / Object来进行判断是否递归即可,本文我们写的更加全面一些,包含了对其他引用类型的判断

function deepClone(obj) {
  if (obj === null) return null // typeof null === 'object'
  if (typeof obj !== 'object') return obj
  // typeof === 'object' 的情形
  var newObj = obj.constructor() // 保持继承链
  for (var key in obj) {
    // 数组是下标,对象是key,hasOwnProperty都能取到,而Date,RegExp,Function取不到。且不考虑原型属性
    if (newObj.hasOwnProperty(key)) {
      // 使用arguments.callee将函数名解耦
      newObj[key] = typeof obj[key] === 'object' ? arguments.callee(obj[key]) : obj[key]
    } else {
      newObj[key] = obj[key]
    }
  }
  return newObj
}

遍历
1)for-in:返回能通过对象访问的、可枚举的属性。包括实例属性和原型属性
2)Object.keys(obj):返回对象的实例属性,不包含原型属性。如果属性是数字或可转成数字类型的字符串,会默认按数字从小到大排序输出。
3)Object.getOwnPropertyNames(obj):返回对象自身所有实例属性和原型属性

类型判断
(1)实例 instanceof 构造函数instanceof操作符用来判断构造函数的原型对象是否在实例的原型链上
(2)实例.constructor === 构造函数:执行此行代码实质上是在执行实例.__proto__.constructor === 构造函数 是否成立
(3)typeof 实例:只能用来判断是否是引用类型,不能判断是否是对象。


# Array 类型

Array类型也是ECMAScript中最常使用的数据类型之一,数组是Array类型的实例,ECMAScript数组类型与其他语言的数组有较大差别。它支持每一项存储任何类型的数据,并且数组长度可以动态调整,该长度会随着数据的添加自动增长来容纳新增数据。

创建
(1)new后面跟构造函数:var colors = new Array();
(2)字面量表示法:var color = []
  (1)方法中,如果预先传入了长度new Array(3),则,即使未给任意一项赋值,colors.length 的值也会为3。除非手动修改长度或执行了影响长度的逻辑,如赋值了第四项。初始化时这三项值都是undefined
  另外,构造函数方式也可以省略new操作符,也支持传入预先设定好的数组项。如var colors = Array('red', 'blue')

读取和设置
(1)读取:var c0 = colors[0]
  如果下标小于length,返回下标对应项的值。大于等于length返回undefined
(2)设置:color[1] = 'pink'
  如果下标小于length,覆盖下标对应项的值。大于等于lengthlength扩容至该下标加1,并将该值赋值给该下标。这是由ECMAScript数组长度可变特性所提供的。

数组的length属性可以用来在数组末位一处或增加新项。

检测数组类型 (经典问题)
(1)value instanceof Array:该操作符的MDN描述如下

The instanceof operator tests the presence of constructor.prototype in object's prototype chain

即:检查value的原型链中是否存在Array构造函数的的原型对象,对于Object或Array而言,原型链上只有一环,instanceof操作符相当于判断如下关系是否成立:

value.__proto__ === Array.prototype

(2)Array.isArray(value):ES6语法,isArray()是构造函数Array的实例方法,能解决跨全局执行环境问题。

常见转换方法
(1)toString():返回数组中每个值的字符串形式拼接而成的一个以逗号分隔的字符串。
  该方法有个经典问题作为拓展如下:

var a = {
  i: 1,
  toString: function() {
    return a.i++
  }
}

if (arr == 1 && arr == 2 && arr == 3) {
  console.log(true)
} else {
  console.log(false)
}
// true

结果很意外的输出了true。看起来有点莫名其妙,其实在执行arr == 时候,会默认执行它的a.toString()方法,而我们重写了该方法,第一次执行后,由于 a.i++特性先返回了1再执行加法,变成2,第二次第三次执行类似。
  该案例主要是考察读者对 arr == 的执行原理的理解以及toString()相对于数据结构 a 的存在关系。

(2)valueOf():返回该数组
(3)toLocaleString():转换成字符串。他与toString()唯一区别在于,它为数组的每一项执行的是toLocaleString()方法而非toString()方法。开发者可以重写toLocaleString()方法来实现特殊的结果。
(4)join():拼接。默认的toString()方法返回的是以逗号分隔的字符串,join()方法允许自定义分隔的内容如空格,斜杆等。当没有参数时,默认为逗号。

栈方法
  栈是一种后进先出LIFO的数据结构,每一项的推入和弹出都是发生在栈的顶部

  • push()推入:该方法可以接受任意是哪个的参数,把他们逐个添加到末尾,并返回修改后数组的长度
  • pop()弹出:该方法从数组末尾移除最后一项,减少lenth值,然后返回移除的项
var color = ['red', 'blue']
var count= color.push('green', 'pink')
console.log(count); // 4
console.log(color); // ['red', 'blue', 'green', 'pink']

var item = color.pop();
console.log(item); // 'pink'

队列方法
  队列方法是先进先出FIFO的数据结构,队列在列表的末尾添加项,在前端移除项。

  • push()推入:同栈方法
  • shift()弹出:从队列头部弹出项,减少数组长度,并返回被移除的项。
  • unshift()反向压入:从队列头压入项。支持多个参数,比返回数组的长度。

重排序方法

  • reverse():反转数组项的顺序。
  • sort():按升序从小到大排列数组项。注意该方法会对数组每一项调用toString()方法,然后比较两项的字符串格式大小,因此对于数字类型而言,会有13小于2的情况。sort()支持传入一个函数参数,用来指定排序的规则,当A项需要排列在B项之前时,返回负数值,相等返回0,A需要排列在B之后时,返回正数值。A/B两项并不局限于数字类型
function compare(A, B) {
  A === B ? 0 : A > B ? 1 : -1
}
// 如果A、B是数字类型,可简化为
funtion compare(A, B) {
  return A - B
}

操作方法

  • concat():该方法不修改原数组,它从原数组中创建一个副本后进行拼接项操作。支持传入多个参数,如果传入的不是数组类型,则会简单的被拼接到副本之后。
var A = [1, 2, 3]
var B = c.concat(4, [5, 6], {a: 7, b: 8})
console.log(B)  // [1, 2, 3, 4, 5, 6, {a: 7, b: 8}]
console.log(A)  // [1, 2, 3]
  • slice()切片: 不影响原数组。接收一个参数时,返回该位置到末尾的所有项;接收两个参数时,返回起始位置到结束位置的项。如果传入了负数,则使用数组长度加上该负数来确定位置。
  • splice():最强大的数组方法,支持任意位置删除一或多项,插入一或多项,替换一或多项。执行何种操作取决于传入了几个参数。
    • 2个参数--删除:要删除的第一项位置及删除的项数,如 splice(0, 2)
    • >=3个参数--插入:起始位置、0(要删除0项)、要插入的项。如splice(1, 0, 'x', 'y')
    • 3个参数--替换:当第二项不是0时,就表示的是替换操作。将第二项是要删除的项数替换成从第三项开始拼接入数组中。如splice(1, 2, 'x', 'y')

位置方法

  • indexOf():返回要查找的项在数组中的位置下标。从0开始往后查找。没有找到返回-1。
  • lastIndexOf():同上,但是是从数组length-1开始从后往前查找。同样返回在数组中的下标。

遍历迭代方法

  • every():对每一项执行指定的函数,如果每一项都返回true,则返回true。可以用作数组遍历中提前终止循环,手动控制某一项返回false即可。
  • some():数组每一项在执行指定函数时有一项返回true,则返回true。同样可以用作遍历提前结束。
  • forEach():该数组没有返回值,对数组每一项执行指定函数。注意forEach不能使用breakcontinue来提前结束,如确实需要,可以使用以上两个方法的技巧来实现。
  • map():对数组中每一项执行给定函数,返回函数调用结果组成的数组。最常用作来返回对象数组中的执行项。如返回booklistid组成的数组。有点过滤每一项中部分信息的意思。
  • filter():对数组每一项执行给定函数,返回这些项中符合条件的项。

  以上五个方法前三个比较好理解,map()filter()比较容易混淆用法,filter()是针对数组的项本身进行过滤,返回的项数通常都小于数组自身。而map()是针对项中的部分属性进行过滤,返回的项数通常等于自身。

var bookList = [
  { id: 100, name: '语文', price: 25 },
  { id: 101, name: '数学', price: 30 },
  { id: 102, name: '英语', price: 35 }
]
booklist.map(book => book.id); // [100, 101, 102]
booklist.filter(book => book.id > 101); // [{ id: 102, name: '英语', price: 35 }]

归并方法

  • reduce():四个参数:前一个值,当前值,项的索引和数组对象自身,通常只用到前两个。将上一次执行结果传入下一次迭代,作为下次迭代的'前一个值'。直到所有项全部执行结束之后,返回最终执行结果。
  • reduceRight():同上,只是迭代从数组最后开始,遍历到第一项上。
var arr = [1, 2, 3, 4]
var sum = arr.reduce((prev, cur, index, arr) => {
  return prev + cur 
})
console.log(sum); // 10

推荐阅读更多精彩内容