引用类型

介绍 ECMAScript5 引用类型的一些基本用法,包括(Object, Array, Date, RegExp、Function)。不包含ES6 新的API

Object类型

  • 点语法和方括号

一般来说,访问对象属性时使用的都是点表示法,这样是很多面向对象语言中的通用用法。不过JavaScript也可以使用方括号表示法来访问对象属性。

const person = {
  name: '纤风',
  age: 1
}
console.log(person.name) // 纤风
console.log(person['name']) // 纤风

从功能上来看,这两种访问对象的方式没有任何区别。但方括号的主要优势是可以通过变量来访问属性

let propertyName = 'name'
console.log(person[propertyName]) // 纤风

如果属性名中包含会导致语法错误的字符,或者属性名使用的是关键字或保留字,也可以使用方括号表示法,比如:

person['first name'] = 'Taylor'

通常情况:除非必须使用变量来访问属性,否则推荐使用点表示法

Array类型

数组的创建
/* 使用Array构造函数 */
let arr1 = new Array()
let arr2 = new Array(20) // 穿件并指定 length的长度
let arr3 = new Array('red', 'cyan', 'yellow') // 传递数组中应该包含的项
// 此外在使用构造函数Array创建数组的时候,可以省略new操作符
let arr4 = Array('green')

/* 字面量方式创建 */
let colors = ['red', 'pink', 'violet'] // 创建一个包含3个字符串的数组

超出数组长度或者未定义的项 都为undefined

let colors = ['cyan', 'skybule', 'hotpink']
console.log(colors[3]) // undefined
colors.length = 4
console.log(colors[3]) // undefined

length属性运用

// 在数组尾部添加元素
let colors = ['red', 'bule']
colors[colors.length] = 'white'
colors[colors.length] = 'yellowgreen'
colors[colors.length] = 'violet'
// 访问最后一个元素
console.log(colors[colors.length - 1])
检测数组
  • instanceof
// 对于一个网页或一个全局作用域而言,使用instanceof操作符就能    得到满意的结果
if (value instanceof Array) {
  // todo
}

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

  • isArray
// 为了解决这个问题, ECMAScript5 新增了 Array.isArray()方法
if (Array.isArray(value)) {
  // todo
}

支持Array.isArray()方法的浏览器有 IE9+、Firefox4+、Safari5+、 Opera10.5+和Chrome

  • Object.prototype.toString
/* 在任何值上调用 Object 原生的 toString() 方法,都会返回一个
[ object NativeConstructorName ]格式的字符串。
每个类在内部有一个[ [class] ]属性,这个属性中就指定了上述字符  串中的构造函数名。
*/
function isArray(value) {
  return Object.prototype.toString.call(value) === "[object Array]"
}
栈方法

ECMAScript 数组也提供了一种让数组的行为类似于其他数据结构的方法,具体来说数组可以表现得像栈一样。
栈是一种 LIFO(Last-In-First-out,后进先出)的数据结构。栈中项的插入(叫做推入)和移除(叫做弹出)

// 使用 push() 和 pop() 方法,模拟栈的行为
let colors = new Array() // 创建一个数组
let count = colors.push('pink', 'bule') // 推入两项
console.log(count) // 2

count = colors.push('black') // 推入另一项
console.log(count) // 3

let item = colors.pop() // 取得最后一项
console.log(item)  // black
console.log(colors.length)  //2
队列方法

栈数据的访问规则是LIFO(后进先出),而队列数据数据结构的访问规则是FIFO(First-In-First-Out, 先进先出)。队列在列表的末端添加,从列表的前端移除项

// 使用 push() 和 shift(), 模拟队列
let colors = new Array()

let count = colors.push('red', 'green') // 推入两项
console.log(count) // 2

count = colors.push('black')  // 推入一项
console.log(count) // 3

const item = colors.shift()  // 取得第一项
console.log(item)  // 'red'
console.log(colors.length) // 2

也可以使用 unshift() 和 pop() 来模拟队列

重排序方法

数组中已经存在两个可以直接用来排序的方法:reverse() 和 sort()

// reverse() 用于 反转数组
let arr = [1, 2, 3, 4, 5, 6]
arr.reverse()
console.log(arr) // [5, 4, 3, 2, 1]

reverse() 的作用相对直观,但是不够灵活,因此有了 sort()方法

/*
sort() 方法会调用每个数组项的 toString() 转型方法,然后比较得到    的字符串,以确定如何排序
即使数组中的每一项都是数值,sort() 方法比较的也是字符串,如下所示。
*/
let arr = [1, 2, 3, 5, 9, 10]
arr.sort()
console.log(arr) // [1, 10, 2, 3, 5, 9]

可见,虽然数组5小于数值100,但在进行字符串比较是,“10”位于“5”的前面,于是数组的顺序就被修改了。这种排序方式在很多情况下都不是最佳方案。因此sort() 方法可以接收一个比较函数作为参数

/*
比较函数接受两个参数
  - 如果第一个参数 应该位于 第二个参数之前 则返回一个负数
  - 如果两个参数相等 返回 0
  - 如果 第一个参数 应该位于 第二个参数之后 则返回一个正数
*/
function compare(a, b) { // 默认升序
  if (a > b) { // 第一个 位于 第二个 之后
    return 1
  } else if ( a < b) {  // 第一个 位于 第二个 之前
    return -1
  } else { // 相等
    return 0
  }
}
// 这个比较函数 适用于大多数数据类型, 只要将其作为参数传递给sort() 即可

let values = [0, 1, 5, 10, 15]
values.sort(compare)
console.log(values) // [0, 1, 5, 10, 15]

对之修改,降序的比较函数,相反即可

function compare(a, b) {
 if (a > b ) {
  return -1
 } else if ( a < b) {
  return 1
 } else {
  return 0
 }
}

对于数值类型或者其他 valueOf() 方法会犯浑数值类型的对象类型,可以使用一个更简单的比较函数,这个函数只要用第二个值减第一个值即可

// 由于比较函数通过返回一个小于零、等于 或者 大于零的值来影响  排序结果
// 因此 减法操作就可以适当处理所有这些情况
function compare(a, b) {
  return a - b
}
操作方法

ECMAScript 为操作已经包含在数组中的项提供了很多的方法。

  • concat()
    concat()方法可以基于当前数组中的所有项创建一个新数组。具体来说,这个方法会先创建当前数组一个副本,然后将接收到的参数添加到这个副本的末尾、最后返回新建的数组。没有添加参数则返回当前数组。如果传递的值不是数组,这些值就会被简单的添加到结果数组的末尾。
  let colors = ['red']
  let colors2 = colors.concat()
  let colors3 = colors.concat('red')
  let colors4 = colors.concat(['cyan', 'green'])
  let colors5 = colors.concat(['cyan', 'green'], 'violet')
  console.log(colors2) // ["red"]
  console.log(colors3) // ["red", "red"]
  console.log(colors4) //  ["red", "cyan", "green"]
  console.log(colors5) //  ["red", "cyan", "green", "violet"]
  • slice()
    基于当前数组的一个或多个项创建一个新数组。接受一个或两个参数,即要返回项的起始和结束位置(不包括结束位置项)。只要一个参数的情况下,返回该位置到数组末尾的所有项。如果结束位置小于起始位置,则返回空数组。注意:slice()不会影响到原数组
let colors = ['red', 'green', 'blue']
let colors2 = colors.slice(1)
let colors3 = colors.slice(0, 2)
let colors4 = colors.slice(2, 1)

console.log(colors2) // ['green', 'blue']
console.log(colors3) // ['red', 'green']
console.log(colors4) // []
  • splice()

    splice() 方法算得上是比较强大的数组方法了,它有很多种用法。splice()的主要作用是向数组中部插入项。

  1. 删除: 可以删除任意数量的项,只需要指定2个参数:删除开始的位置 和 要删除的数量
let colors = ['red', 'green', 'pink', 'gray']
let item = colors.splice(0, 2) // 移除前两项, 返回被移除的两项
console.log(item) //  ["red", "green"]
console.log(colors) // ["pink", "gray"]
  1. 插入: 可以向指定位置插入任意数量的项,只需要提供三个参数:起始位置,0(要删除的项数),要插入的项
let colors = ['red', 'green', 'pink', 'gray']
let res = colors.splice(2, 0, 'black', 'white')
console.log(res) // 返回的是一个空数组
console.log(colors) // ["red", "green", "black", "white", "pink", "gray"]
  1. 替换,可以向指定位置插入任意数量的项,且同时删除任意数量的项。
let colors = ['white', 'black']
let res = colors.splice(0,1, 'darkless')
console.log(res) // 返回被删除的项 ["white"]
console.log(colors) //  ["darkless", "black"]
位置方法
  • indexOf() 和 lastIndeOf()
    这两个方法都是返回要查找的项在数组中的位置,或者在没有找到的情况下返回-1。lastIndexOf() 是从数组的末端向前找
let numbers = [1, 2, 3, 4, 5, 6]
console.log(numbers.indexOf(2)) // 1
console.log(numbers.lastIndexOf(2)) // 1  
迭代方法

ECMAScript 为数组定义了 5个迭代方法。每个方法都接受量两个参数:要在每一项上运行的函数和(可选的)运行该函数的作用域对象——影响this的值。

  1. every()
    对数组中的每一项给定函数,如果该函数对每一项都返回true,则返回true
let numbers = [1, 2, 3, 4, 5, 6, 7] 
let everyRes = numbers.every(function(item, index, array) {
  return (item > 2)
})
console.log(everyRes) // false
  1. some()
    对数组中的每一项运行给定函数,如果该函数对任一项返回 true,则返回 true。
let numbers = [1, 2, 3, 4, 5, 6, 7] 
let someResult = numbers.some(function(item, index, array) {
  return (item > 2)
})
console.log(someResult ) // true
  1. filter()
    对数组中的每一项运行给定函数,返回该函数会返回true的项组成数组
let numbers = [1, 2, 3, 4, 5, 6, 7] 
let filterResult= numbers.filter(function(item, index, array) {
  return (item > 2)
})
console.log(filterResult) // [3, 4, 5, 6, 7]
  1. map()
    对数组中的每一项运行给定函数,返回每次函数调用的结果组成 数组。
let numbers = [1, 2, 3, 4, 5, 6, 7] 
let mapResult= numbers.map(function(item, index, array) {
  return (item * 2)
})
console.log(mapResult) //  [2, 4, 6, 8, 10, 12, 14]
  1. forEach()
    对数组中的每一项运行传入的函数。这个方法没有返回值,本质上与使用for循环迭代数组一样
let numbers = [1, 2, 3, 4, 5, 6, 7] 
numbers.forEach(function(iten, index, array) {
  // todo
})
归并方法

ECMAScript5 还新增了两个归并数组的方法: reduce() 和 reduceRight()。 这两个方法都会迭代数组的所有项,然后构建一个最终返回值。其中 reduceRight() 是从右边开始。
这两个方法都接受两个参数: 一个在每一项上调用的函数 和 (可选的)作为归并基础的初始值。

let arr = [1, 2, 3, 4, 5]
let sum = arr.reduce(function(prev, cur, index, array) {
// prev 前一个值(数组第一个元素的 prev 是第二个参数的默认值(0))
// cur 当前项
// index 当前下标
// 数组对象
  return prev + cur
}, 0)
console.log(sum)

reduceRight() 的作用类似,只不过方向相反而已。使用 reduce() 和 reduceRight(), 主要取决于要从哪头开始。除此之外, 他们完全相等。 (ps:支持这两个函数的浏览器有:IE9+、Firefox3+、Safari4+、Opera10.5+ 和 Chrome)

Date类型

ECMAScript中的Date类型是在早期 Java中的java.util.Date 类基础上构建的。为此,Date类型使用自UTC(Coordinated Universal Time,国际协调时间)1970年1月1日午夜(零时)开始经过的毫秒数保存日期,Date类型保存的日期能够精确到1970年1月1日之后或之前的1000 000 000年。

要创建一个日期对象,需要通过new操作符和 Date 构造函数即可

var now = new Date()

在调用Date() 构造函数不传递参数的情况下,新建的日期对象自动获取当前日期和时间
如果想根据特定的日期和时间创建日期对象,必须传入表示该日期的毫秒数(即从UTC时间1970年1月1日午夜起至该日期止经过的毫秒数)。为了简化这一计算过程,ECMAScript提供了两个方法:Date.parse() 和 Date.UTC()

  • Date.parse()
    接受一个标识日期的字符串参数,根据此参数返回毫秒数。通常接受下列日期格式。
    • “月/日/年“, 如 6/13/2004;
    • “英文月名 日,年”,如January 12, 2004;
    • “英文星期几 英文月名 日 年 时:分:秒 时区”,如 Tue May 25 2004 00:00:00 GMT-0700。
    • ISO 8601 扩展格式 YYYY-MM-DDTHH:mm:ss:sssZ(l例如 204-05-25T00:00:00)。只要兼容ECMAScript5的支持这种格式
// 为 2004年5月25日创建一个日期对象,可以使用下面的代码
let time = Date.parse('May 25, 2004') // 1085414400000
// 如果传入Date.parse()方法的字符串不能表示日期,那么它会返回NaN.
let someDate = new Date(time) // Tue May 25 2004 00:00:00 GMT+0800 (香港标准时间)

// 实际上直接将表示日期的字符串传递给Date构造函数,也会在后台调用Date.parse()
let otherDate = new Date('May 25, 2004') // Tue May 25 2004 00:00:00 GMT+0800 (香港标准时间)

日期对象及其在不同浏览器中的实现有许多奇怪的行为。其中有一种倾向是将超越出范围的值替当前的值,以便生成输出。例如,在解析“January 32,2007”时有的浏览器会将其解释为“February 1,2007”。而Opera则倾向于插入当前月份当前日期。发挥“January 当前日期,2007”

  • Date.UTC()
    Date.UTC() 的参数分别年份、基于0的月份(1月是0)、月中的哪一天(1到31)、小时数(0到23)、分钟、秒、毫秒数。只要前两个参数(年和月)是必须的。天数默认值为一天,其他默认为0
// GMT时间 2000年1月1日午夜零时
let y2k = new Date(Date.UTC(2000, 0)) 

// GMT时间2005年5月5日下午5:55:55
let allFives = new Date(Date.UTC(2005, 4, 5,17, 55, 55))

如同模仿Date.parse() 一样,Date 构造函数也会模仿Date.UTC(),但是有一点明显不同:日期和时间都基于本地时区而非GMT创建。不过,Date构造函数接收的参数任然与Date.UTC() 相同。因此,如果第一个参数是数值,Date构造函数会假定改值是 日期中的年份,第二个参数是月份,以此类推

let y2k = new Date(2000, 0)
let allFives = new Date(2005, 4, 5, 17, 55 ,55)

以上代码创建了与前面例子中相同的两个日期对象,只不过这次的日期都是基于系统设置的本地时区创建的

  • Date.now()
    ECMAScript5中添加了 Date.now() 方法,返回表示调用这个方法时间的日期和时间的毫秒数
let start = Date.now()

// todo

let stop = Date.now()

支持Date.now()方法的浏览器包括 IE9+、Firefox3+、Safari3+、Opear10.5、Chrome
在不支持它的浏览器中,使用 + 操作符获取Date对象的时间戳也可以达到同样的目的

let start = + new Date()

// todo

let stop = + new Date()

此外 console.time() 和 console.timeEnd() 也可以实现

// 开始计时
console.time('test');

// todo

// 停止计时
console.timeEnd('test');
继承的方法

与其他引用类型一样,Date类型也重写了 toLocaleString()、toString() 和 valueOf() 方法。Date类型的toLocaleString()方法会按照与浏览器设置地区相适应的格式返回日期和时间。这大致意味着时间格式中会包含AM或PM,但不会包含时区信息(具体格式会因浏览器而异)。而toString()方法则通常返回带有时区信息的日期和时间。

// 输出PST(Pacific Standard Time, 太平洋标准时间)2007年2月1日午夜零时结果
// Internet Explorer 8
toLocaleString() - Thursday, February 01, 2007 12:00:00 AM
toString() - Thu Feb 1 00:00:00 PST 2007

至于Date类型valueOf() 方法,则根本不返回字符串,而是返回日期的毫秒表示。因此,可以方便使用比较操作符来比较日期

let day1 = new Date(2007, 0, 1)
let day2 = new Date(2007, 1, 1)

console.log(day2 > day1) // true
日期格式化方法

Date类型还有一些专门用于格式化日期字字符串的方法。

  • toDateString() - 以特定于实现的格式显示星期几、月、日和年
  • toTimeString() - 以特定实现的格式显示时、分、秒和时区
  • toLocaleDateString() - 以特定于时区的格式显示星期几、月、日和年
  • toLocaleTimeString() - 以特定于实现的格式显示时、分、秒
  • toLocaleString() - 以特定于实现的格式完成的UTC日期。
    同样,以上这些字符串格式方法的输出也是因浏览器而异的,因此没有哪一个方法能够用来在用户界面展示一致的日期

到目前为止。剩下的还未介绍的Date类型的方法,都是直接缺德和而设置日期值中特定部分的方法了。需要注意的是,UTC日期指的是在没有时区偏差情况下的日期值(中国大陆、中国香港、中国的时间与UTC的时差均为+8,也就是UTC+8)
Mozilla文档

RegExp类型

ECMAScript 通过 RegExp类型来支持正则表达式。使用下面 类型 Perl的语法,就可以创建一个正则表达式
const expression = / pattern / flags
其中 pattern 是任何简单或复杂的正则表达式。每个正则表达式都可以带有一个或多个标志(flags),用以表明正则表达式的行为。

  • 正在表达式的匹配模式支持下列3个标志
    • g:表示全局(global)模式,即模式将被应用u所有字符串,而非在发现第一个匹配项时立即停止;
    • i:表示不区分大小写(case-insensitive) 模式,即在确定匹配项时忽略模式与字符串的大小写
    • m:表示多行(multiline)模式,即在到达一行文本末尾还会继续查找下一行中是否存在于模式匹配的项
      与其他语言中的正则表达式类似,模式中使用的所有元字符都需要转义。正则的元字符包括: ( [ { \ ^ $ | ) ? * + . ] }
// 匹配第一个"bat" 或 "cat", 不区分大小写
let pattern1 = /[ba]at/i

// 匹配第一个"[ba]at", 不区分大小写
let pattern2 = /\[ba\]at/i

上面的例子都是使用字面量的方式创建。此外还可以通过构造函数的方式创建

// RegExp构造函数接受两个参数:匹配的字符串模式、标志字符串
let pattern1 = new RegExp('[bc]at', 'i')
// 由于RegExp构造函数的模式参数是字符串,所有默写情况下要针对字符进行双重转义
// 例如\n(字符\在字符串中通常被转义为 \\, 而在正则表达式字符串中就会变成\\\)
let pattern2 = new RegExp('\\[bc\\]at', 'i')
RegExp实例属性

RegExp的每个实例都具有下列属性,通过这些属性可以获取有关模式的各种信息

  • global:布尔值,表示是否设置了g标志
  • ignoreCase:布尔值,表示是否设置了i标志。
  • lastIndex:整数,表示开始搜索下一个匹配项的字符位置,从0算起
  • multiline:布尔值,表示是否设置了m 标志
  • source:正则表达式的字符串表示,按照字面量形式而非传入构造函数中的字符串模式返回。
var pattern = /\[bc\]at/i
console.log(pattern.global) // false
console.log(pattern.ignoreCase) // true
console.log(pattern.lastIndex) // 0
console.log(pattern.multiline) // false
console.log(pattern.source) // \[bc\]at
RegExp 实例方法
  • exec()
    exec()接受一个参数,即要应用模式的字符串,然后返回第一个匹配项信息的数组
let text = 'is life always this hard, or is it just when you are a kids?'
const pattern = /is (life always this hard), or is it just when (you are a kids)\?/gi
let matches = pattern.exec(text)

/*
0: "is life always this hard, or is it just when you are a kids?"
1: "life always this hard"
2: "you are a kids"
groups: undefined
index: 0
input: "is life always this hard, or is it just when you are a kids?"
length: 3
*/

对于exec()方法而言,即使设置了全局标志(g),他每次也只会返回一个匹配项。在不设置全局标志的情况下,在一个字符串上多次调用exec()将始终返回第一个匹配项的信息。而在设置全局标志的情况下,每次调用exec()则都会在字符串中继续查找新匹配项。

  • test()
    这个方法经常出现在验证用户输入的情况下,大多数我们通过他只想知道输入是不是有效。
let text = '000-000-0000'
let pattern = /\d{3}-\d{3}-\d{4}/
if (pattern.test(text)) {
  // todo
}

RegExp实例继承的toLocaleString 和 tostring 方法都会返回正则表达式的字面量,与创建的方式无关.valueOf() 方法返回正则表达式本身

let pattern = new RegExp('\\[bc\\]at', 'gim')
console.log(pattern.toString()) // /\[bc\]at/gim
console.log(pattern.toLocaleString()) // /\[bc\]at/gim
console.log(pattern.valueOf()) // /\[bc\]at/gim
构造函数属性

RegExp构造函数属性(静态属性)。使用与当前作用域中的所有正则表达式,并且基于执行的最近一次正则表达式操作而变化。关于这些属性的另一个独特之处,就是可以通过两种方式访问他们,他们分别具有一个长属性名和短属性名(Opera不支持短属性名)。

  • input($_):最近一次要匹配的字符串
  • lastMatch($&):最近一次的匹配项
  • lastParen($+):最近一次匹配的捕获组。
  • leftContext($ `):input字符串中lastMatch之前的文本
  • rightContext($'):input字符串中lastMatch之后的文本
let str = 'this will be a short summer'
const pattern = /(.)hort/g

if (pattern.test(str)) {
  console.log(RegExp.input) // this will be a short summer
  console.log(RegExp.leftContext) // this will be a 
  console.log(RegExp.rightContext) //  summer
  console.log(RegExp.lastMatch) // short
  console.log(RegExp.lastParen) // s
}

除了上面介绍的几个属性之外,还有多达9个用于存储捕获的构造函数属性。分别 $1 ... $9,在调用exec() 或 test() 时这些方法会被自动填充

let text = 'Hello World'
const pattern = /(..)ll(.)/g

if (pattern.test(text)) {
  console.log(RegExp.$1) // Ho
  console.log(RegExp.$2) // o
}

Function 类型

在ECMAScript 中 函数是一等公民,而有意思的是,函数实际上是对象。每个函数都是Function类型的实例,而且都与其他引用类型一样具有属性和方法。由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。

函数的声明

函数通常使用函数声明语法定义的,如下:

function sum(num1, num2) {
  return num1 + num2
}

也可以使用函数表达式定义函数,与上面这种方式相差无几

const sum = function(num1, num2) {
  return num1 + num2
}

此外还可以通过构造函数

const sum = new Function('num1', 'num2', 'return num1 + num2')

从技术角度将,这是一个函数表达式。但是,不推荐使用这种方法定义函数,因为这种语法会导致解析两次代码(第一次是解析常规ECMAScript代码,第二是解析传入构造函数中的字符串),
从而影响性能。不过,这种语法对于理解“函数就是对象,函数名就是指针”的概念倒是非常直观

没有重载(深入理解)

将函数名想象为指针,也有助于理解为什么ECMAScript中没有函数重载的概念。

function addSomeNumber(num) {
  return num + 100
}

function addSomeNumber(num) {
  return num + 200
}

let result = addSomeNumber(100) // 300

上面这个例子,声明了两个同名的函数,结果是后面的函数覆盖了前面的函数。

函数声明与函数表达式

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

// 函数声明
console.log(sum(10, 10))
function sum(num1, num2) {
  return num1 + num2
}
// 函数表达式
console.log(sum(10, 10))
const sum = function (num1, num2) {
  return num1 + num2
}

以上两段代码,第一段可以正常运行,第二段则会报错。因为在代码开始执行前,解析器就已经通过一个名为函数提升(function declaration hoisting)的过程,读取并将函数声明添加到执行环境中。反之第二段发生错误,原因在于函数位于一个初始化语句中,而不是一个函数声明。换句话说,在执行到函数所在的语句之前,变量sum中不会保存对函数的引用;而且,由于第一行代码就会导致“unexpected identifier”(意外标识符)错误,实际上也不会执行到下一行
除上之外,两者间是等价的

作为值的函数

因为ECMAScript中的函数名本身就是变量,所有函数也可以作为值来使用。

function callSomeFuntion(someFunction, someArgument) {
  return someFunction(someArgument)
}

function getGreeting(name) {
  return 'Hellow ' + name
}
const result = callSomeFunction(getGreeting, 'liaoFan') 
console.log(result) // Hellow liaoFan

当然,也可以从函数中返回另一个函数。
例如,我们想根据某个对象的属性对数组进行排序。而传递给数组sort()方法的比较函数要接受两个参数,即要比较的值。

function createComparisonFunction(propertyName) {
  return function(object1, object2) {
    const param1 =  object1[propertyName]
    const param2 = object2[propertyName]

    if ( param1 > param2 ) {
      return 1
    } else if(param1 < param2) {
      return -1
    } else {
      return 0
    }
  }  
}

let arr = [
  { age: 17, height: 165, weight: 110},
  { age: 15, height: 175, weight: 130},
  { age: 20, height: 170, weight: 120}
]
arr.sort(createComparisonFunction('age')) // 按照年纪排序
arr.sort(createComparisonFunction('height')) // 按照升高排序
arr.sort(createComparisonFunction('weight')) // 按照体重排序
函数内部的属性

在函数的内部有两个特殊的对象:arguments 和 this。 其中 arguments有一个名叫callee的属性,该属性是一个指针,指向拥有这个 arguments 对象的函数。

// 阶乘的经典函数
function factorial(num) {
  if (num <= 1) {
    return 1
  } else {
    return num * factorial(num - 1)
  }
}

定义阶乘通常都要用到递归算法,如上所示,在函数有名字,而且名字以后也不会变的情况下这样是没有问题的。但是问题是这个函数的执行与函数名factorial紧紧耦合在了一起,为了消除这种紧密耦合的现象,可以像下面一样。

function factorial(num) {
  if (num <= 1) {
    return 1
  }else {
    return num * arguments.callee(num - 1)
  }
}

在这个重写后的factorial()函数的函数体内,没有再引用factorial。这样,无论引用函数时使用的什么名字,都可以保证正常完成递归调用。
this
函数内部的另一个对象就是this,与其他语言大致类似,this引用的是函数执行的环境对象。

window.color = 'red'
const o = { color: 'cyan' }

function getColor() {
  return this.color
}
// this 指向 window
console.log(getColor()) // red

o.getColor = getColor
// this 指向 对象o
console.log(o.getColor()) // cyan

ECMAScript 5也规范了另一个函数对象的属性:caller。这个属性中保存着调用当前函数的函数的引用,如果是在全局作用域中直接调用当前函数,他的值为null

function outer() {
  inner()
}
function inner() {
  console.log(inner.caller)
}
outer() // ƒ outer() { inner() }
inner() // null

为了实现更松散的耦合,也可以通过 arguments.callee.caller来访问相同信息

function outer() {
  inner()
}
function inner() {
  console.log(arguments.callee.caller)
}
outer() // ƒ outer() { inner() }

当函数在严格模式运行时,arguments.callee会导致错误,此外也不能为函数的caller属性赋值,否则也会导致错误。以上这些变化都是为了加强这门语言的安全性,这样第三方代码就不能再相同的环境里窥视其他代码了。

函数属性和方法

前面增加提到,ECMAScript 中的函数时对象,因此函数也有属性和方法。每个函数都包含两个属性:length 和 prototype。其中,length属性表示函数希望接收的命名参数的个数。

function sayName(name) { return name}
function sum (num1, num2) { return num1 + num2}
console.log(sayName.length) // 1
console.log(sum.length) // 2

在ECMAScript核心所定义的全部属性中,最耐人寻味的就要数 prototype 属性了。对应 ECMAScript中的引用类型而言,prototype是保存他们所有实例方法的真正所在。在创建自定义引用类型以及实现继承是,prototype属性的作用是及其重要的。在ECMAScript中,prototype属性是不可枚举的,因此使用 for-in 无法发现

每个函数都包含两个非继承而来的方法:apply() 和 call()。这两个方法的用途是在特定的作用域中调用函数,实际上等于设置函数体内的this对象的值

  • apply()
    apply() 方法接受两个参数:一个是在其运行函数的作用域,另一个是参数数组。其中,第二个参数可以是Array的实例,也可以arguments对象
function sum(num1, num2) {
  return num1 + num2
}
function callSum1 (num1, num2) {
  return sum.apply(this, arguments) // 传入 arguments
}
function callSum2(num1, num2) {
  return sum.apply(this, [num1, num2]) // 传入数组
}

console.log(callSum1 (10, 10))
console.log(callSum2(10, 10))
  • call()
    call()方法与 apply() 方法作用相同,他们的区别在于接受参数的方式不同。call在传递传递参数是,需要逐个列举出来
function sum(num1, num2) {
  return num1 + num2
}
function callSum(num1, num2) {
  return sum.call(this, num1, num2)
}
console.log(callSum(10, 10)) // 20

事实上,传递参数并非apply() 和 call() 真正的用武之地;他们真正强大的地方是能够扩充函数赖以运行的作用域。

window.color = 'violet'
const o = { color: 'yellowgreen' }

function getColor() {
  return this.color
}
getColor() // red
getColor.call(o) // yellowgreen
getColor.apply(o)  // yellowgreen

使用call()和apply()来扩充作用域的最大好处,就是对象不需要与方法有任何耦合关系。在前面的一个例子中,我们需要将 方法 放到对象函数 o中,然后再通过o来调用它,而在这里重写的例子中,就不需要那个多余的步骤了

  • bind()
    在ECMAScript 5中还定义了一个方法:bind()。这个方法会创建一个函数的实例,其this值会被绑定到传给bind()函数的值。
window.color = 'pink'
const o = { color: 'gray' }

function getColor() {
  return this.color
}
// 注意: bind 返回一个函数的实例
const GetObjColor = getColor.bind(o)
GetObjColor() // gray

你可能感兴趣的:(引用类型)