概述
第3章 语法结构
3.10.1 使用let和const声明
ES6后,变量通过let关键字声明
let i
let sum
- 可以使用一条let语句声明多个变量
let i, sum
- 声明变量的同时,(如果可能)也为其赋予一个初始值
let message = 'hello'
let i = 0, j = 1
let 语句中 不为变量指定初始值,变量也会被声明, 但在被赋值之前它的值是undefined
要声明常量而非变量,则要使用const而非let,const与let类似,区别在于const必须在声明时初始化常量
const AU = 1.496E8 // 天文单位
常量是不能改变的,重新赋值会报错 TypeError
何时使用const
- 只有值在基本不会改变的情况下使用const,比如物理常数,程序版本号
- 有时程序中很多所谓的变量实际上在程序运行时不会改变,应用const,如果发现确实需要值改变再改成let
- JS中的for、for/in和for/of循环语句, 其中每种循环都包含一个循环变量,在循环的每次迭代都会取得一个新值
for(let datum of data) console.log(datum)
上述也可以使用const来声明这些循环变量,只要保证在循环体内不给它重新赋值即可,此时const声明只是一次循环迭代期间的常量值
for(const datum of data) console.log(datum)
变量与常量作用域
- 通过let和const声明的变量和常量具有块级作用域,这意味着它们只在let和const语句所在的代码块有定义
- JS 类和函数的函数体是代码块,if/else语句的语句体、while和for循环的循环体都是代码块,
- 作为for、for/in、for/of 循环的一部分声明的变量和常量,以循环体作为它们的作用域
重复声明
- 在同一个作用域中使用多个let或const声明同一个名字是语法错误,在嵌套作用域中声明同名变量却是合法的(不推荐)
const x = 1
if(x === 1) {
let x = 2
console.log(x)
}
console.log(x) // 1 回到全局变量
let x = 3 // 报错,重复声明
声明与类型
- JS的变量声明与值的类型无关,JS变量可以保存任何类型的值
let i = 10
i = 'len' // 给一个变量赋一个数值,然后再给它赋一个字符串是合法的
3.10.2使用var的变量声明
- ES6之前的JavaScript,声明变量的唯一方式是使用var关键字,无法声明常量,var的语法与let语法相同
- var和let有相同的语法,但是它们有重要的区别:
① var声明的变量不具有块级作用域
② 在函数体外部使用var,则会声明一个全局变量。通过var声明的全局变量被实现为全局对象的属性,全局对象可以通过globalThis(全局属性globalThis包含全局的this值,类似于全局对象)引用, 而通过let和const声明的全局变量和常量不是全局对象的属性。
③ 与let声明的变量不同,使用var多次声明同名变量是合法的。var变量具有函数作用域而不是块级作用域,
// var变量不会将这些变量的作用域限定在函数体内,每次循环都会重新声明和重新初始化同一个变量
for(var i = 0; ....) {}
④ var声明的变量,最不寻常的一个特性是,作用域提升(在使用var声明变量时,该声明会被提高到包含函数的顶部)
因此对使用var声明的变量,可以在包含函数内部的任何地方使用而不会报错。如果初始化的代码尚未运行,则变量的值可能是undefined
3.10.3 解构赋值
/*
解构赋值:
- ES6允许按照一定模式从数组和对象中提取值,对变量进行赋值,
- 1、数组的解构
- 2、对象的解构
*/
// 1、数组的解构 []
const F4 = ['小沈阳', '刘能', '赵四', '宋小宝']
let [xiao, liu, zhao, song] = F4
console.log(xiao) // 小沈阳
// 2、对象的解构 {}
const zao = {
nam:'赵本山',
age:'不详',
xiaopin:function() {
console.log('我可以演小品')
}
}
let {nam, age, xiaopin} = zao // 此处用name关键字问题等 对对象里面的值进行一一对应
console.log(nam) // 赵本山
解构赋值让使用返回数组的函数变得异常便捷 - 使得返回数组的函数使用
// 将[x, y] 坐标转换为[r, theta] 极坐标
function toPolar(x, y) {
return [Math.sqrt(x*x, y*y), Math.atan2(y, x)]
}
let [r, theta] = toPolar(1.0, 1.0) // r == Math.sqrt(2) theta == Math.PI/4
可以在JavaScript的各种for循环中声明变量和常量,同样也可以在这个上下文中使用变量解构赋值
let o = {x:1, y:2}
for(const [name, value] of Object.entries(o)) {
console.log(name, value) // 'x 1' 和 'y 2'
}
解构赋值左侧变量的个数不一定与右侧数组中的元素个数相同,左侧多余的变量会被设置为undefined,而右侧多余的值会被忽略 左侧的变量列表可以包含额外的逗号,以跳过右侧的某些值
let [x, y] = [1] // x == 1, y == undefined
[x, y] = [1, 2, 3] // x == 1, y == 2
[, x, , y] // x == 2, y == 4
在解构赋值的时候,如果想把所有未使用或剩余的值收集到一个变量中, 可以在左侧最后一个变量名前面加上3个点 (…)
let [x, ...y] = [1, 2, 3, 4] // y == [2, 3, 4]
解构赋值可用于嵌套函数,此时赋值的左侧看起来也应该像一个嵌套的数组字面量
let [a, [b, c]] = [1, [2, 2.5], 3] // a == 1, b == 2, c == 2.5
第4章 表达式与操作符
链判断运算符(?. 运算符)
- 实际开发中,如果要读取对象内部的某个属性,往往需要先判断该对象是否存在
- 比如,要读取 user.firstName,安全的写法应该是下面这样的:
const firstName = (user && user.firstName) || 'default'
- 使用链判断运算符来简化上述写法
const firstName = user?.firstName || 'default'
// 解释
上述代码使用 ?. 运算符,直接在链式调用的时候判断,左侧对象是否为null或undefined。如果是的,就不再往下运算,而是返回undefined
使用 ?. 运算符的注意点
- 短路机制
a?.[++x]
// 等同于
a == null ? undefined : a[++x]
// 上述代码中,如果a是undefined或者null, 那么x不会进行递增运算,也就是说链判断运算符一旦为真,右侧表达式就不再求值
Null判断运算符
- 读取对象属性的时候,如果某个属性的值是 null 或 undefined,有时候需要为它们指定默认值。常见做法是通过 || 运算符指定默认值。
const text = data.text || 'Hello, world!'
- 上面的代码都通过 || 运算符指定默认值,但是这样写可能和预期的结果不一致。
- 开发者的原意是,只要属性的值为 null 或 undefined,默认值就会生效,但是属性的值如果为空字符串或 false 或 0,默认值也会生效。
- 为了避免这种情况,ES2020 引入了一个新的 Null 判断运算符??。它的行为类似 ||,但是只有运算符左侧的值为 null 或 undefined 时,才会返回右侧的值。
const text = data.text ?? 'Hello, world!'
关键点
- Null判断运算符有一个目的就是可以跟链判断运算符 ?. 配合使用,为 null 或 undefined 的值设置默认值。
const animationDuration = settings?.animationDuration ?? 300
上面代码中,settings 如果是 null 或 undefined,就会返回默认值300。
4.13.5 await操作符
- ES2017新加的,用于让JavaScript异步编程更加的自然
- 简单来说,await期待一个Promise对象(表示异步计算)作为其唯一的操作数,可以让代码看起来好像是在等待异步计算完成(但实际上它不会阻塞其他异步操作进行)
- await操作符的值是Promise对象的兑现值,关键在于,await只能出现在已经通过async关键字声明为异步的函数中,即 await和async总是搭配进行使用的
第5章 语句
for/of
- for/of循环专门用于可迭代对象【目前只要知道数组,字符串,集合和映射都是可迭代的即可】
- 如何迭代一个数值数组并计算所有数值之和
// 1、for/of
let data = [1, 2, 3, 4, 5, 6, 7, 8, 9], sum = 0
for(let element of data) {
sum += element
}
console.log(sum) // 45
- 数组迭代是“实时的” ,即迭代过程中的变化可能影响迭代的输出。如果修改前面的代码,在循环内添加一行,data.push(sum)则会创建一个无穷循环
for/of 与 对象 - 对象默认是不可迭代的,运行时尝试对常规对象使用for/of会抛出 TypeError
let o = {x:1, y:2, z:3}
for(let element of o) {
console.log(element) // TypeError: o is not iterable 报错 !!!
}
- 如果想迭代对象的属性,可以使用for/in 循环,或者基于**Object.keys()**方法的结果使用for/of
let o = {x:1, y:2, z:3}
let keys = ''
for(let k of Object.keys(o)) {
console.log(k)
keys += k
}
console.log(keys) // 'xyz'
console.log(typeof keys) // string
- 这是因为Object.keys() 返回一个对象属性名的数组,而数组是可以通过for/of 来迭代的
- 这种对象的键的迭代并不想之前那个例子一样是实时的,在循环体内修改对象o不会影响迭代
- 也可以使用Object.values() 来获取每个键对应的值
let o = {x:1, y:2, z:3}
let sum = 0
for(let v of Object.values(o)) {
sum += v
}
console.log(sum) // 6
- 也可以使用Object.entries() 来同时解构键值来使用for/of 遍历
let o = {x:1, y:2, z:3}
let pairs = ''
for(let [k, v] of Object.entries(o)) {
pairs += k + v // 前面相连已经变成字符串了
// console.log('@', pairs)
}
console.log(pairs) // 'x1y2z3'
Object.entries() 返回一个数组的数组 ,其中每个内部数组表示对象的一个属性的键/值对
let o = {x:1, y:2, z:3}
console.log(Object.entries(o)) // [ [ 'x', 1 ], [ 'y', 2 ], [ 'z', 3 ] ]
for/of 与 字符串
- 字符串在ES6中可以逐个字符迭代的
let frequency = {}
for(let letter of 'mississippi') {
if(frequency[letter]) {
frequency[letter]++
// console.log(frequency)
}else {
frequency[letter] = 1 // 一开始没有该属性的时候,键值对中的值设置为1
}
}
console.log(frequency) // { m: 1, i: 4, s: 4, p: 2 }
for/of 与 Set 和 Map
- ES6 内置的Set(集合)和 Map(映射)类是可迭代的,在使用for/of 迭代Set时,循环体对集合中的每个元素都会运行一次。
- 可以使用类似下面的代码打印一个文本字符串中的唯一单词
let text = 'na na na na na na kang'
let wordSet = new Set(text.split(' '))
let unique = []
for(let word of wordSet) {
unique.push(word)
}
console.log(unique) // [ 'na', 'kang' ]
Map比较有意思,因为Map对象迭代器并不迭代Map键或Map值, 而是迭代键值对 每次迭代,迭代器都会返回一个数组, 其第一个元素是键,第二个元素是对应的值
- 给出一个Map, 可以像下面这样迭代和解构 其 键/值对
let m = new Map([[1, 'one']])
console.log('@', m) // { 1 => 'one' }
for(let [key, value] of m) {
console.log(key) // 1
console.log(value) // one
}
for/await 与 异步迭代
- ES2018 新增了一种迭代器,称为异步迭代器,同时新增了一种for/of循环,使用异步迭代器 for/await 循环
- 第12、13章会重点介绍,先看代码示例
// 从异步可迭代流中读取数据块并将其打印出来
async function printStream(stream) {
for await (let chunk of stream) {
console.log(chunk)
}
}
for/in
- for/in 循环的in后面可以是任意对象,前面讲到的for/of 后面必须是可迭代对象
for/in 语句可以循环指定对象的属性名
- 比如可以这样使用for/in 循环语句
for(let p in o) { // 将o的属性名赋值给变量p
console.log(o[p]) // 打印每个属性的值
}
- for/in 循环体中的variable可能是任意表达式,主要能求值为赋值表达式的左值就可以
- 这个表达式在每次循环时,都会被求值,这意味着每次的求值结果可能都不同
- 例:把一个对象的所有属性值复制到数组中
let o = { x : 1, y : 2, z : 3}
let a = [ ], i = 0
for(a[i++] in o) // 空循环体
在操作数组时,基本只会用到for/of 而不是 for/in
很多程序员更愿意基于Object.keys() 使用 for/of ,而非使用for/in 循环
跳转语句
- 跳转语句会导致JavaScript解释器跳转到源代码中的新位置
- 1、break语句会让解释器跳转到循环末尾或跳转到其他语句
- 2、continue语句会让解释器跳出循环体并返回循环顶部开始新一轮迭代
- 3、return语句会让解释器从函数调用跳转回调用位置,同时提供调用返回的值
- 4、yield语句是一种在生成器函数中间返回的语句
- 5、throw会抛出异常,设计用来与try/catch/finally 语句共同使用,后者可以构成异常处理代码块(抛出异常是一种复杂的跳转语句)
return
- 函数调用是表达式,而所有表达式都有值,函数中的return 语句指定了函数调用的返回值
- 语法
return expression
- return语句只能出现在函数体内
- 执行return语句后,包含它的函数向调用者返回expression的值
- 如果没有return语句,函数调用者会依次执行函数体中的每个语句,直至函数末尾,然后返回给其调用者,此时,调用表达式求值为undefined
- return语句通常是函数中最后一条语句,但并非必须是最后一条,函数在执行时,只要执行到return语句,就会返回到其调用者,而不管这个return语句后面是否还有其他语句
- return语句后面也可以不带expression,从而导致函数向调用者安徽undefined
yield
- yield非常类似于return语句,但只能在ES6新增的生成器函数中,以返回生产的值序列中的下一个值,同时又不会真正返回
/*
回送一系列整数的生成器序列
*/
function* range(from, to) {
for (let i = from; i <= to; i++) {
yield i
}
}
range(1, 7)
console.log(range(1, 7)) // Object [Generator] {}
- 为了理解yield,必须理解迭代器和生成器,yield是一个操作符,而非语句
迭代器 - 任何数据结构,只要部署了Iterator接口,就可以完成遍历操作
- 在获取数组对象中的元素的时候,用Iterator.next()
/*
迭代器介绍
*/
// 声明一个数组
const xiyou = ['唐僧', '孙悟空', '猪八戒', '沙僧']
// 获取一个对象
let iterator = xiyou[Symbol.iterator]() // Symbol.iterator为Iterator接口
// 调用对象的 next() 方法来获取数据
console.log(iterator.next()) // { value: '唐僧', done: false } -- done值表示还没有迭代完成
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next()) // { value: undefined, done: true }
生成器
- 生成器其实就是一个特殊的函数
- 异步编程,纯回调函数
- 其中的yield可以看成是 函数demo的分隔符
/*
生成器介绍
*/
function * gen(){
// yield其实可以看成是函数代码的分割符
console.log(111)
yield '一只没有耳朵' // 分割
console.log(222)
yield '一只没有尾巴'
console.log(333)
yield '真奇怪'
console.log(444)
}
// 执行比较特殊
let iterator = gen()
// console.log(iterator) // 直接调也不能走
// 必须调用next方法
// iterator.next() // hello generator
// 使得原有代码一句一句执行
// iterator.next()
// iterator.next()
// iterator.next()
// iterator.next()
// 遍历
for(let v of gen()){
// 一次性全部打印出来
console.log(v)
}
/*
遍历的结果:
111
一只没有耳朵
222
一只没有尾巴
333
真奇怪
444
*/
// 但是使用iterator.next() 是遍历产生每次到分隔符那个位置的内容
throw
- 异常是一种信号,表示发生了某种意外情形或错误
- 抛出异常是为了表明发生了这种错误或者意外情形,捕获(catch)异常则是要处理它,即采取必要或对应的措施以从异常中恢复
- 在JS中,每当运行时发生错误或者程序里使用throw语句时都会抛出异常
- 可以使用try/catch/finally语句来捕获异常
throw语法
throw expression
- expression可能求值为任何类型的值,可以抛出一个表示错误码的数值,也可以抛出一个包含可读错误消息的字符串
- JS解释器在抛出错误时会使用Error类及其子类,当然我们也可以在自己的代码中使用这些类
- Error对象有一个name属性和一个message属性,分别用于指定错误类型和保存传入构造函数的字符串
- 例子:在收到无效参数时,抛出一个Error对象
function factorial(x) {
// 如果收到的参数无效,则抛出异常
if(x < 0) throw new Error('x must not be negative')
// 否则 计算一个值 并正常返回,
let f
for(f = 1; x > 1; f *= x, x--) {
/* 空语句 */
// console.log(f)
}
return f
}
let result = factorial(4)
console.log(result) // 24
- 抛出异常时,JS解释器会立即停止正常程序的运行并调到最近 的异常处理程序
- 异常处理程序是使用try/catch/finally语句中的catch来处理
- 如果发生异常的代码块没有关联的catch子句,解释器会检查最接近的上一层代码块,看是否有与之关联的异常处理程序,这个过程一直延续,直到找到处理程序
- 如果函数中抛出了异常,但函数体内没有处理这个异常的try/catch/finally 语句,则异常会向上传播到调用函数的代码,在这种情况下,异常是沿着JS方法的词法结构和调用栈向上传播
- 如果没有找到任何异常处理程序,则将异常作为错误报告给用户
try/catch/finally
- try/catch/finally是JS中的异常处理机制
- try子句用于定义要处理其中异常的代码块
- try块后面紧跟着catch子句,catch是一个语句块,在try块中发生异常时会被调用
- catch子句后面是finally快,其中包含清理代码,无论try块中发生了什么,这个块中的代码一定会执行
- catch和finally块都是可选的,但只要有try块,就必须有它们两中的一个
- try/catch/finally块都以花括号开头和结尾,花括号是语法要求的部分,即使语句块只包含一条语句也不能省略
try{
/*
正常情况下,这里的代码会从到尾执行
不会出现问题,但有时候也可能抛出异常
直接通过throw语句抛出,或者由于调用了一个抛出异常的方法而抛出
*/
}catch(e) {
/*
当且仅当try快抛出异常时,才会执行这个
块中的语句,这里的语句可以使用局部变量
e引用被抛出的Error对象,这个块可以以某种方式来处理异常
也可以什么都不做以忽略
异常,还可以通过throw重新抛出异常
*/
}finally{
/*
无论try块中发生了什么,
这个语句块包含的语句都会被执行,无论try块是否终止,这些语句都会被执行
1、正常情况下,在到达try块底部时执行
2、由于break、continue、return 语句而执行
3、由于上面的catch子句处理了异常而执行
4、由于异常未被处理而继续传播而执行
*/
}
注意:
catch关键字后面通常跟着一个包含在圆括号中的标识符,这个标识符类似函数的参数,当捕获到异常时,与异常关联的值(比如一个Error对象)就会被赋给这个参数,与catch子句关联的标识符具有块作用域,即只在catch块中有定义
实例try/catch例子
try {
// 请输入一个数值
let n = Number(prompt("please enter a positive", ""))
// 假设输入有效 计算该数值的阶层
let f = factorial(n)
// 显示结果
alert(n + "!= " + f)
} catch (error) { // 如果用户的输入无效,会跳到这里
alert(error)
}
对finally行为的再解释
- 只要执行了try块中的任何代码,finally子句就一定会执行,无论try块中的代码是怎么执行完的。
- finally子句经常用于执行完try子句之后执行代码清理
- 如果解释器由于break、return语句离开了try块,则解释器在跳转到新目标之前会执行finally块
- 如果try块发生了异常,而且有关联的catch块来处理这个异常,则解释器会先执行catch块,然后再执行finally块
- 如果局部没有catch块处理异常,则解释器会先执行finally块,然后再跳转到最接近的包含catch子句
- 如果finally子句抛出异常,该异常会代替正被抛出的其他异常
- 如果finally子句执行了return语句,则相应方法正常返回, 即使有被抛出且尚未处理的异常
- try和finally可以配对使用,而不带catch子句
其他语句(with、debugger、use strict)
1、with(尽量不用)
- with会运行一个代码块,就好像指定对象属性
- 语法
with(object)
statement
这个语句创建了一个临时作用域,以object的属性作为变量,然后在这个作用域中执行statement
2、debugger
- debugger语句一般什么也不做,
- 包含debugger的程序在运行时,实现可以执行某种调试操作
- 实践中,这个语句就像一个断点,执行中的JS会停止,我们可以使用调试器打印变量的值、检查调用栈,等等
- 例如:假设你在调用函数f() 时没有传参数,函数就会抛出异常,而你不知道这个调用来自何处,为了调试这个问题,可以修改f(),如下面
function f(o) {
if(o == undefined) debugger // 仅为调试才添加的
... // 函数中的其他代码
}
现在,再次调用f() 而不传参数,执行就会停止,你可以使用调试器检查调用栈,找到这个错误的调用来自何处
注意:只有调试器还不行,debugger语句并不会为你打开调试器。如果你使用浏览器并且打开了开发者控制台,这个语句就会导致断点
3、use strict
use strict 指令的目的是表示(在脚本或函数中)它后面的代码是严格代码!
声明
- 关键字const、let、var、function、class、import、export 严格来讲不是语句,而应该叫做声明
- 语句会导致“某些事情发生”,声明可以定义新值并给他们命名,以便通过这个名字来引用相应的值
- 宽泛的说,可以把声明看成程序的一部分,这一部分在代码运行前会预先处理
const、let和var
- 在ES6及之后的版本中,const声明常量而let声明变量
- ES6之前,使用var是唯一一个声明变量的方式,无法声明常量
- 使用var声明的变量,其作用域为包含函数(leetcode声明函数都是用var声明的),而非块,这可能会导致隐含的错误
function
- function声明用于定义函数,函数声明的例子
function area(radius) {
return Math.PI * radius * radius
}
- 函数声明会创建一个函数对象,并把这个函数对象赋值给指定的名字(在这里四area)
- 然后在程序中任意位置可以通过这个名字引用这个函数
- 位于任何JS中的函数声明都会在代码运行之前被处理,而在整个代码块中函数名都会绑定到相应的函数对象
- 无论在作用域中的什么地方声明函数,这些函数都会被“提升”,就好像他们是在该作用域的顶部定义的一样,于是在程序中,调用函数的代码可能位于函数声明的代码之前
class
- 在ES6及之后的版本,class声明会创建一个新类并为其赋予一个名字,以便将来引用,第九章会详细介绍类
class Circle {
constructor(radius) {
this.r = radius
}
area() {
return Math.PI * radius * radius
}
circumference(){
return 2 * Math.PI * this.r
}
}
注意:与函数不同,类声明不会被提升,因此在代码中,不能在还没有声明类之前就使用类
import 和 export
- import 和 export声明共同用于让一个JavaScript模块中定义的值可以在另一个模块中使用
- 一个模块就是一个JavaScript代码文件,有自己的全局作用域,完全与其他模块无关
- 如果要在一个模块中使用另一个模块中定义的值(函数或类),唯一的方式就是在定义值的模块中使用export导出值,再使用值的模块中使用import 导入值
- import 指令用于从另一个JS代码文件中导入一个或多个值,并在当前模块中为这些值指定名字,import指令有几种不同的形式,例如
import Circle from './geometry.circle.js'
import {PI, TAU} from './geometry/constants.js'
import {magnitude as hypotenuse} from './vectors/utils.js'
JavaScript模块中的值是私有的,除非被显示导出,否则其他模块都无法进行导入,export指令就是为此而生的
- 其声明把当前模块中定义的一个或多个值导出,因而其他模块可以导入这些值
const PI = Math.PI
const TAU = 2 * PI
export {PI, TAU}
- export 关键字有时候也用于其他声明的标识符**,从而构成一种复合声明,在定义常量、变量、函数、类的同时又导出它们**
- 如果一个模块只导出一个值,通常会使用特殊的 export default 形式
export const TAU = 2 * Math.PI
export function ...
export default class Circle {...}
第6章 对象
6.1对象简介
- 对象是一个属性的无序集合,每个属性都有名字和值
- 对象不仅仅是简单的字符串到值的映射,除了维持自己的属性之外,JS对象也可以从其他对象继承属性,这个其他对象称其为“原型”。对象的方法通常是继承来的属性。
有时候, 区分直接定义在对象上的属性和那些从原型对象上继承的属性很重要。JS使用术语 “自有属性” 指代非继承属性。
除了名字和值之外,每个属性还有3个属性特征:
① writable(可写)特性指定是否可以设置属性的值
② enumerable(可枚举)特性指定是否可以在 for/in 循环中返回属性的名字
③ configurable (可配置)特性指定是否可以删除属性,以及是否可修改其特性
6.2 创建对象
创建对象的方式(3种)
- 对象字面量
- new关键字
- Object.create()
6.2.1 对象字面量
对象字面量的最简单形式是包含在一对花括号中的一组逗号分隔开来的 “名 : 值”对形式
let empty = {} // 没有属性的对象
// 包含两个数值的属性
let point = {
x : 1,
y : 0,
}
6.10 对象字面量扩展语法 (原型)
6.10.1 简写属性
// 之前写法
let x = 1, y = 2
let o = {
x : x,
y : y,
}
// ES6之后写法
let x = 1, y = 2
let o = { x, y }
o.x + o.y = 3
6.2.2 使用new创建对象
- new操作符用于创建和初始化一个对象,new关键字后面必须跟一个函数调用
- 以这种方式使用的函数称为构造函数,目的是初始化新创建对象
- JS为内置的类型提供了构造函数
let o = new Object() // 创建一个空对象 {}
let a = new Array() // 创建一个空数组 []
let d = new Date() // 创建一个表示当前时间的日期对象
let r = new Map() // 创建一个映射对象
除了内置构造函数,我们经常需要定义自己的构造函数来初始化新创建的对象
6.2.3 原型
- 每个JS对象都有另一个与之关联的对象,这另一个对象被称为原型,第一个对象从这个原型继承属性!
- 通过对象字面量创建的所有对象都有相同的原型对象,在JS中可以通过Object.prototype引用这个原型对象!
- 使用new Object() 创建的对象继承自 Object.prototype 与通过 {} 创建的对象一样,类似的Array() 创建的对象以Array.prototype为原型,通过new Date() 创建的对象以Date.prototype为原型
- **切记:**几乎所有对象都有原型(Object.prototype是为数不多的没有原型的对象,因为它不继承任何属性,其他原型对象都是常规对象,都有自己的原型),但只有少数对象有prototype属性,正是这些有prototype属性的对象为所有其他对象定义了原型
多数内置构造函数(和多数用户定义的构造函数)的原型都继承自Object.prototype - 例如:Date.prototype 从Object.prototype继承属性,因此通过new Date() 创建的日期对象从 Date.prototype和Object.prototype继承属性。这种原型对象链接起来的序列被称为原型链!
Object.assign()
- ES6新增语法,用于合并对象
let a = {
kang:''
}
let b = {
kang:'kang'
}
console.log(Object.assign(a,b)) // { kang: 'kang' }
Object.create()
- Object.create()用来创建一个新对象,使用其第一个参数作为新对象的原型
let o1 = Object.create({x:1, y:2}) // o1继承属性x和y
o1.x + o1.y // 3
- 传入null值可以创建一个没有原型的新对象,但是这样创建的新对象不会继承任何东西,连toString() 这种基本方法都没有(意味着不能对该对象应用 +操作符)
let o2 = Object.create(null) // o2不继承任何属性或方法
- 如果想创建一个普通的空对象(类似{}或new Object() 返回的对象,)传入Object.prototype
let o3 = Object.create(Object.prototype) // o3 与 {} 或 new Object() 类似
- 能够以任意原型创建新对象是一种非常强大的技术(Object.create()还可以接收可选的第二个参数,用于描述新对象的属性,这个参数属于高级特性)
- Object.create()的一个用途:防止对象被某个第三方库函数意外修改,这种情况下,不要直接把对象传给库函数,而要传入一个继承自它的对象。如果函数读取这个对象的属性,可以读到继承的值。但是要设置这个对象的属性,则修改不会影响原始对象。
let o = {x :"don't change this value"}
library.function(Object.create(o)) // 防止意外修改
查询和设置属性
- 要获得一个属性的值,可以使用(.)或方括号([])操作符
let author = book.author // 取得book的'author'属性
let name = author.surname // 取得author的'surname' 属性
let title = book['main title'] // 取得book的'main title'属性
- 要创建或设置属性,与查询函数一样,可以使用点或方括号,只是要把它们放到赋值表达式的左边
book.edition = 7 // 为book创建一个'edition'属性
book['main title'] = 'ECMAScript' // 修改"main title"属性
使用方括号时,其中的表达式必须求值为一个字符串,更准确的说法是,该表达式必须求值为一个字符串或一个可以转换为字符串或符号的值 - 在下一章,我们看到方括号中使用数字也是很常见的
作为关联数组的对象
- 如前所述,下面两个JavaScript表达式的值相同
① object.property
② object['property']
第二种语法使用方括号和字符串,看起来像访问数组,只不过是以字符串的形式而非数值作为索引的数组,这种数组被称为关联s
序列化对象
- 对象序列化是把对象的状态转换为字符串的过程,之后可以从中恢复对象的状态。
- JSON.stringify()用于序列化,JSON.parse()用于恢复JS对象 这两个函数使用JSON数据交换格式
- JSON(Java对象表示法)其语法与JavaScript对象和数组字面量非常类似
let o = {x:1, y:{z:[false, null, ""]}} // 定义一个测试对象
let s = JSON.stringify(o)
console.log(s) // {"x":1,"y":{"z":[false,null,""]}}
let p = JSON.parse(s)
console.log(p) // { x: 1, y: { z: [ false, null, '' ] } }
第7章 数组
7.1 创建数组
基于浏览器F12测试
let digits = [...'abc']
digits
(3) ['a', 'b', 'c']
集合对象是可迭代的,因此要去除数组中的重复元素,一种便捷方式就是先把数组转换为集合,再使用扩展操作符将这个集合转换成数组
let letters = [...'hello world']
[...new Set(letters)]
(8) ['h', 'e', 'l', 'o', ' ', 'w', 'r', 'd']
Array() 构造函数
- 不传参数调用
let a = new Array()
- 传入一个数组参数,指定长度
let a = new Array(10)
- 传入两个或多个数组元素,或传入一个非数值元素
let a = new Array(5, 4, 3, 2, 1, 'testing')
Array.of()
- 使用数组参数调用Array() 构造函数时,这个参数指定的是数组长度, Array() 构造函数无法创建只包含一个数值元素的数组
- ES6中,Array.of() 函数可以解决这个问题,其为工厂方法,可以使用其参数值作为数组元素来创建并返回新数组
Array.of(1)
[1]
Array.of(10)
[10]
Array.of(1,2,3,'hello')
(4) [1, 2, 3, 'hello']
Array.from()
- ES6中新增的另一个工厂方法, 其期待一个可迭代对象或类数组对象作为其第一个参数,并返回包含该对象元素的新数组,是一个创建数组副本的简单形式
let origin = [1, 2, 3]
let copy = Array.from(origin)
copy
(3) [1, 2, 3]
- 其定义了一种给类数组对象创建真正的数组副本的机制
- 类数组对象不是数组对象,但也有一个数值length属性,每个属性的键也都是整数,
- 在客户端JavaScript中,有些浏览器方法返回的值就是类数组对象,那么可以先把它们转换成真正的数组便于后续的操作
let trueArray = Array.from(arrayLike)
读写数组元素
- 数组索引其实就是一种特殊的对象属性,所以JavaScript数组中没有所谓 “越界” 错误,
- 查询任何对象中不存在的属性都不会导致错误,只会返回undefined,数组作为一种特殊对象也是如此
let a = [true, false]
a[2]
undefined
a[-1]
undefined
稀疏数组
- 稀疏数组就是其元素没有从0开始的索引的数组,
- 数组是稀疏的,则length属性的值会大于元素的个数
- 真的碰到了稀疏矩阵,可以把稀疏矩阵当成包含undefined元素的非稀疏矩阵
数组长度
b = [1, 2, 3, 4, 5]
(5) [1, 2, 3, 4, 5]
b.length = 3
3
b
(3) [1, 2, 3]
b.length = 0 // 删除所有元素 b是[]
0
b.length = 5 // 长度为5,但没有元素,类似 new Array(5)
5
b
(5) [空属性 × 5]
添加和删除数组元素
- 对新索引赋值
let a = []
a[0] = 'zero'
- 使用push()方法在数组末尾添加一个或多个元素 (**与push() 执行相反操作的是pop()方法,它删除数组最后一个元素并返回该元素,同时导致数组长度-1 **)
let a = []
a.push('zero')
a.push('one', 'two')
- 要在数组开头插入值,可以使用unshift() 方法(在开头删除值 使用shift() 删除并返回数组的第一个元素)
可以使用delete操作符删除数组元素
- 使用delete操作符不会修改length属性,即从数组中删除元素后,数组会变稀疏
迭代数组
到ES6为止,遍历一个数组的最简单方式就是使用for/of 循环
let letters = [..."Hello world"]
let string = ""
for(let letter of letters){
// string += letter
console.log(letter)
}
// console.log(string)
对于稀疏数组,这个循环没有特殊行为,凡是不存在的元素都返回undefined
如果要对数组使用for/of循环,并且想知道每个数组元素的索引,可以使用数组的entries() 方法和解构赋值
let letters = [..."Hello world"]
for(let [index, letter] of letters.entries()) {
console.log('@', index) // 索引从0开始
console.log('#', letter)
}
forEach()
- 并非新的for循环,而是数组提供的一种用于自身迭代的函数式方法
- 需要给forEach() 传一个函数,然后forEach() 会用数组的每个元素调用一次这个函数
let letters = [..."Hello world"]
let uppercase = ''
letters.forEach(letter => {
uppercase += letter.toUpperCase()
});
console.log(uppercase) // HELLO WORLD
当然使用老式的for循环也可以遍历数组
for(let i = 0; i < letters.length; i++){
console.log(letters[i])
}
当数组是稀疏的,我们想要跳过未定义或不存在的元素时
for(let i = 0; i < letters.length; i++){
if(letters[i] === undefined) continue
}
二维数组
let table = new Array(10)
for(let i = 0; i < table.length; i++) {
table[i] = new Array(10)
}
console.log(table)
二维数组用的较少,访问二维数组的时候使用matrix[row][col]
数组方法
数组迭代器方法
forEach()
- forEach() 方法迭代数组的每个元素,并对每一个元素都调用一次我们指定的函数,
- 传统forEach() 方法的第一个参数是函数,forEach() 在调用这个函数时会给他它传3个参数:数组元素的值、数组元素的索引、数组本身
- 如果只关心数组元素的值,可以把函数写成只接收一个参数,忽略其他参数
<script>
let data = [1, 2, 3, 4, 5], sum = 0
// 计算数组元素之和 - 只关心数组元素的值,可以把函数写成只接收一个参数
data.forEach(value => {
sum += value
})
console.log(sum) // 15
/*
forEach() 第一个参数是函数
该函数有三个参数
- 1、数组元素的值
- 2、数组元素的索引
- 3、数组本身
*/
// 递增每个元素的值
data.forEach(function(v, i, a){
a[i] = v + 1
})
console.log(data) // [2, 3, 4, 5, 6]
</script>
map()
- map() 方法把调用它的数组的每个元素分别传给我们指定的函数,返回这个函数的返回值构成的数组
<script>
let a = [1, 2, 3]
b = a.map( x => x * x)
console.log(a) // [1, 2, 3]
console.log(b) // [1, 4, 9]
</script>
- map() 返回一个新数组,并不修改调用它的数组, 想要修改的话可以使用原数组进行修改操作
- 如果数组是稀疏的,缺失元素不会调用我们的函数,但返回的数组也会与原始数组一样稀疏,长度相同,缺失的元素也相同
filter()
- filter() 方法返回一个数组,该数组包含调用它的子数组,传给这个方法的函数应该是一个断言函数(返回true和false函数)
<script>
let a = [5, 4, 3, 2, 1]
b = a.filter( x => x < 3)
console.log(a) // [5, 4, 3, 2, 1]
console.log(b) // [2, 1]
</script>
- 该函数返回内部逻辑为true的子数组,且该方法不会对原数组进行修改操作,返回一个新的数组
- 注意:filter()函数会跳过稀疏数组中缺失的数组,返回的数组始终都是稠密的, 因此可以使用filter() 方法来处理掉稀疏矩阵中的空隙
let dense = sparse.filter(() => true)
let a = [5, 4, , , 1]
let b = a.filter(() => true) // [5, 4, 1]
console.log(b)
- 如果即想清理空隙又想删除值为undefined和null的元素,可以这样,过滤掉里面的内容即可
a = a.filter( x => x !== undefined && x!== null)
find() 与 findIndex()
- 与filter() 类似,但是这两个方法会在断言函数中找到第一个元素时停止迭代
- find() 返回匹配的元素 - 没找到返回undefined
- findIndex() 返回匹配元素的索引 - 没找到返回-1
let a = [1, 2, 3, 4, 5]
console.log(a.findIndex(x => x === 3)) // 2
console.log(a.find(x => x % 5 === 0)) // 5
every() 与 some()
- 都是数组断言方法,对数组元素调用我们传入的断言函数,最后返回true或false
- every() 同数学上的 任意
- some() 同数学上的存在
every
let a = [1, 2, 3, 4, 5]
console.log(a.every(x => x < 10)) // true
console.log(a.every(x => x % 2 === 0)) // false 并非所有值都是偶数
some
let a = [1, 2, 3, 4, 5]
a.some(x => x % 2 === 0) // true
注意:
- every() 和 some() 都会在它们知道要返回什么值时停止迭代数组,如some() 在断言函数第一次返回true时返回true,只有全部返回false才会遍历数组
reduce() 与 reduceRight()
- reduce() 与 reduceRight() 使用我们指定的函数归并数组元素,最终产生一个值
reduce() 接收两个参数
- 第一个参数:执行归并操作的函数—> 任务:把两个值 归并或组合为一个值并返回这个值,
- 第二个参数:可选的,是传給归并函数的初始值
let a = [1, 2, 3, 4, 5]
console.log(a.reduce((x, y) => x + y, 0)) // 15
console.log(a.reduce((x, y) => x * y, 1)) // 120
console.log(a.reduce((x, y) => (x > y) ? x : y)) // 5
- 如果不传入初始值,在空数组调用reduce() 会导致typeError
doneTotal() {
return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0);
},
在此处函数里面的第二个参数又是一个对象
reduceRight()
- 与reduce() 类似,只不过是从高索引向低索引来处理数组
使用flat() 和 flatMap() 打平数组
flat()
- 不传参调用时,默认是打平一层嵌套
[1, [2, 3]].flat() // [1, 2, 3]
- 想要打平更多层级,需要给flat() 传一个数组参数
let a = [1, [2, [3, [4]]]]
a.flat(2) // [1, 2, 3, [4]]
flatMap()
- 与map() 方法类似,只不过返回的数组会自动被打平,就像传给了是flat()
let phrases = ["hello world", "my love"]
let words = phrases.flatMap((item) => item.split(""))
console.log(words)
结果
[
'h', 'e', 'l', 'l', 'o',
' ', 'w', 'o', 'r', 'l',
'd', 'm', 'y', ' ', 'l',
'o', 'v', 'e'
]
使用concat() 添加数组
- concat() 方法创建并返回一个新数组,并不修改调用它的数组
- 新数组包含concat() 方法的数组元素,以及传给concat() 的参数
- 并不会打平嵌套的数组
let a = [1, 2, 3]
console.log(a.concat(4, 5)) // [ 1, 2, 3, 4, 5 ]
console.log(a.concat([4, 5], [6, 7]))
console.log(a.concat(4, [5, [6, 7]])) // [ 1, 2, 3, 4, 5, [ 6, 7 ] ]
console.log(a) // [1, 2, 3]
说白了,就是只能打平一级的数组
通过push() 、pop()、shift() 和 unshift() 实现栈和队列的操作
push() & pop()
- push() 或者 pop() 可以把数组作为栈来操作
- push() 方法用于在数组末尾添加一个或多个新元素,并返回数组的新长度, pop() 用于删除数组最后的元素
- 与concat() 不同 push() 不会打平数组参数
let stack = []
stack.push(1, 2)
console.log(stack) // [1, 2]
stack.pop()
console.log(stack) // [1]
- push() 方法不会打平传入的数组,如果想把一个数组中的所有元素 都推送到另一个数组中,可以使用扩展操作符显式打平
a.push(...value)
unshift() & shift()
- 从数组开头而非末尾插入和删除元素
- unshift() 用于在数组开头添加一个或多个元素,已有元素索引会向更高索引移动,并返回数组新长度
- shift() 删除并返回数组的第一个元素
- 可以使用push() 在数组末尾添加元素,使用shift() 在数组开头删除元素来实现队列
let q = []
let len = q.push(1, 2)
console.log(len) // 2
q.shift()
console.log(q) // [2]
- unshift() 有一个很重要的特性:在给unshift() 传多个参数的时候,这些参数会一次性插入数组,这意味着一次插入与多次插入之后的数组顺序是不一样的
let a = []
a.unshift(1)
a.unshift(2) // [2, 1]
a = []
a.unshift(1, 2) // [1, 2]
使用slice() 、splice() 、fill() 、copywith()
以上是数组定义的处理连续区域(数组切片)的方法
1、slice()
- slice() 方法返回一个数组的切片或者子数组
- 两个参数:指定切片的开始位置和结束位置**(取不到)** ① 只指定一个参数:该起点直到数组末尾的所有元素 ② 其中任何一个参数为负数,则这个值相对于数组长度来指定数组元素(如:参数-1指定数组的最后一个元素)
- slice() 不会修改调用它的数组
let a = [1, 2, 3, 4, 5]
a.slice(0, 3) // [1, 2, 3]
a.slice(3) // [4, 5]
a.slice(1, -1) // [2, 3, 4]
console.log(a.slice(-3, -2)) // [3]
2、splice()
删除操作:
- splice() 是一个对数组进行插入和删除的通用方法
- 与slice() 和 concat() 不同, splice() 会修改调用它的数组
- 作用:① 从数组中删除元素 ② 向数组插入新元素 ③ 可以同时执行这两种操作
- 位于插入点和删除点之后的元素的索引会按照需要增大或减小,从而与数组剩余部分保持连续,
- 参数:参数一:指定插入或删除的起点位置 参数二:指定要从数组中删除的元素个数(如果省略,从起点元素开始的所有数组元素将被删除)
- splice() 返回被删除元素的数组,如果没有删除元素返回空数组
let a = [1, 2, 3, 4, 5, 6, 7, 8]
console.log(a.splice(4)) // [5, 6, 7, 8]
console.log(a) // [1, 2, 3, 4]
a.splice(1, 2) // [2, 3] , a现在是[1, 4]
插入操作
- 前两个参数指定要删除哪些元素,这两个参数后面可以跟任意多个参数,表示要在第一个参数指定的位置插入到数组中的元素,例如:
let a = [1, 2, 3, 4, 5]
a.splice(2, 0, 'a', 'b') // [], a现在是[1, 2, 'a', 'b', 3, 4, 5]
a.splice(2, 2, [1, 2], 3) // ['a', 'b'], a现在是[1, 2, [1, 2], 3, 3, 4, 5]
3、fill()
- fill() 方法将数组的元素或切片设置为指定的值
- 修改调用它的数组, 也返回修改后的数组
- 参数:参数一:要把数组元素设置成的值, 可选参数二:指定起始索引(如果省略,则从索引0开始填充) 可选参数三:指定终止索引,到这个索引为止(但不包含)的数组元素会被填充(如果省略直接填充到末尾)
let a = new Array(5) // 创建一个长度为5的没有元素的数组
a.fill(0) // [0, 0, 0, 0, 0] 用0来填充数组
a.fill(9, 1) // [0, 9, 9, 9, 9]
a.fill(8, 2, -1) // [0, 9, 8, 8, 9]
**4、copyWithin()**用的较少
- copyWithin() 把数组切片复制到数组中的新位置
- 其会就地修改数组并返回修改后的数组,但不会改变数组长度
- 参数:参数一:指定要把第一个元素复制到的目的索引 参数二:指定要复制的第一个元素索引(省略的话默认为0) 参数三:指定要复制的元素切片的终止位置(包含)
let a = [1, 2, 3, 4, 5]
a.copyWithin(1) // [1, 1, 2, 3, 4] 把数组元素复制到索引及之后
a.copyWithin(2, 3, 5) // [1, 1, 3, 4, 4] 把最后两个元素复制到索引2
a.copyWithin(0, -2) // [4, 4, 3, 4, 4] 负偏移也可以
数组索引和排序方法
1、indexOf() 和 lastIndexOf()
- 从数组中搜索指定的值并返回找到第一个元素的索引,如果没有找到返回-1
- indexOf() 从前往后, lastIndexOf()从后往前
let a = [0, 1, 2, 1, 0]
a.indexOf(1) // 1
a.lastIndexOf(1) // 3
a.indexOf(3) // -1
注意: indexOf()和lastIndexOf() 都接收第二个可选的参数,指定从哪个位置开始搜索,如果省略这个参数,indexOf() 从头开始搜索,lastIndexOf() 从尾开始搜索
let a = [1, 2, 3, 4, 1, 2]
console.log(a.indexOf(1, 2)) // 4
/*
从数组a中找到所有值x,返回匹配索引的数组
*/
function findAll(a, x) {
let result = []
len = a.length
pos = 0
while(pos < len) {
pos = a.indexOf(x, pos)
if(pos === -1) break // 如果没有找到,结束即可
result.push(pos) // 把索引保存在数组中
pos += 1
}
return result
}
console.log(findAll([1, 2, 3, 1, 1, 2], 1)) // [0, 3, 4]
字符串也有indexOf() 和 lastIndexOf() 方法,和这两个数组方法类似,区别在于第二个参数如果是负数会被当成是 0
**2、includes() **
- ES2016的includes() 方法接收一个参数,如果数组包含该值则返回true,否则返回false,并不会告诉你值的索引
- 实际上就是测试数组的成员是否属于某个集合
- indexOf() 无法检测到数组中的NaN值,但是includes() 可以
let a = [1, true, 3, NaN]
a.includes(true) // true
a.includes(2) // false
a.includes(NaN) // true
a.indexOf(NaN) // -1 indexOf无法找到NaN的值
3、sort()
- sort() 对数组元素就地排序并返回排序后的数组,不传递参数时,sort() 按字母顺序对数组元素排序
let a = ['banana', 'apple']
a.sort() // a == ['apple', 'banana']
- 要对数组元素执行非字母顺序的排序,必须给sort() 传递一个比较函数作为参数
let a = [33, 4, 1111, 222]
a.sort()
a.sort(function(a, b) { // 传入一个比较函数
return a - b // 取决于顺序,返回 <0、0、>0
}) // a == [4, 33, 222, 1111]
a.sort((a, b) => b - a) // a == [1111, 222, 33, 4]
- 如果相对字符串数组做不区分大小写的字母排序,传入的比较函数应该先使用(toLowerCase() 方法)
4、reverse()
- reverse() 反转数组元素的顺序,并返回反序后的数组
- 不会用重新排序后的元素创建新数组,而是直接对已经存在的数组重新排序
let a = [1, 2, 3]
a.reverse() // a == [3, 2, 1]
数组到字符串的转换
Array类定义了3个把数组转换为字符串的方法,通常可以用在记录日志或错误信息的时候
**join() **
- join() 方法把数组的所有元素转换为字符串,然后把他们拼接起来并返回结果字符串
- 可以指定一个可选的字符串参数,用于分隔结果字符串中的元素,如果不指定分隔符,则默认使用逗号
let a = [1, 2, 3]
a.join() // '1,2,3'
a.join(" ") // '1 2 3'
a.join("") // '123'
let b = new Array(10)
b.join("-") // '---------'
String.split()
join() 方法执行的是String.split() 的反向操作,后者通过把字符串分割为多个片段来创建数组 split() 方法中需要放入的是字符串形式的
let res = 'hello world'
res.split("") // (11) ['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd']
"2:3:4".split(':') // (3) ['2', '3', '4']
toString() - 对于数组而言,该方法的逻辑和没有参数的join方法是一致的
[1, 2, 3].toString()
'1,2,3'
['a', 'b', 'c'].toString()
'a,b,c'
[1, [2, 'c']].toString()
'1,2,c'
静态数组函数
Array类也定义了3个静态函数,可以通过Array构造函数而非数组调用 Array.of() 和 Array.from() 是创建新数组的工厂方法,之前介绍过,另一个静态数组函数是Array.isArray() 用于确定一个未知值是不是数组
Array.isArray([]) // true
Array.isArray({}) // false
类数组对象
JavaScript数组具有一些其他对象不具备的特殊特性
- 数组的length属性会在新元素加入时自动更新
- 设置length属性为更小的值会截断数组
- 数组从Array.prototype继承有用的方法
- Array.isArray() 对数组返回true
以上特性让JavaScript数组与常规对象有了明显区别,但这些特性并非定义数组的本质特性 事实上只要对象有一个数值属性length,而且有相应的非负整数属性,就完全可以视同为数组
下面的代码会为一个常规对象添加属性,让它成为一个类数组对象,然后再遍历得到伪数组的“元素”
let a = {} // 创建一个常规的空对象
// 添加属性让它变成“类数组”对象
let i = 0
while(i < 10) {
a[i] = i * i
i++
}
/*
只要对象有一个数值属性length,而且有相应的非负整数属性
那就可以完全可以视同为数组
*/
a.length = i
// 像遍历真正的数组一样遍历这个对象
let total = 0
for(let j = 0; j < a.length; j++) {
total += a[j]
}
console.log(total) // 285
注意:
- 在客户端JavaScript中,很多操作HTML文档的方法(document.querySelectorAll())都返回类数组对象,下面的函数可以用来测试对象是不是类数组对象
- 测试:是否为类数组对象:
/*
确定对象o是不是类数组对象
*/
function isArrayLike(o) {
if (o
&& typeof o === 'object' &&
Number.isFinite(o.length) &&
o.length >= 0 &&
Number.isInteger(o.length) &&
o.length < 4294967295) {
return true
} else {
return false
}
}
- 字符串的行为与数组类似,但是上述function对字符串肯定返回false
- 字符串最好还是最为字符串而非数组来处理
多数数组方法有意设计成了泛型方法,因此除了真正的数组,同样可以用于类数组对象,但由于类数组对象不会继承Array.prototype,所以无法直接在他们身上调用数组方法,因此可以使用Function.call() 方法
let a = {'0':'a', '1':'b', '2':'c', length:3} // 类数组对象
Array.prototype.join.call(a, '+') // 'a+b+c'
Array.prototype.slice.call(a, 0) // ['a', 'b', 'c']
Array.from(a) // ['a', 'b', 'c'] 更容易的数组复制
倒数第二行代码在类数组对象调用了Array的slice方法,把该对象的元素复制到一个真正的数组对象中,在很多遗留demo中这是常见的习惯做法, 但是现在使用Array.from() 会更容易
作为数组的字符串
- JavaScript字符串的行为类似于UTF-16 Unicode字符的只读数组,除了使用charAt() 方法访问个别字符,还可以使用方括号语法
let s = 'test'
s.charAt(0) // 't'
s[1] // 'e'
当然对字符串而言,typeof操作符仍然返回’string’, 把字符串传给Array.isArray()方法仍然返回false
重要:
- 字符串与数组的行为类似意味着我们可以对字符串使用泛型的字符串方法
Array.prototype.join.call('JavaScript', ' ')
'J a v a S c r i p t'
- 切记:字符串是不可修改的值,因为把他们当成数组来使用时,他们只是只读数组,向push()、sort() 、reverse() 这些就地修改数组的数组方法不生效!
第8章 函数
- 函数是一个JavaScript代码块,定义之后,可以被执行或调用任意多次
- 函数定义可以包含一组标识符,称为**参数或形参,**这些形参类似于函数体内定义的局部变量,函数调用会为这些形参提供值或实参
- 除了实参,每个调用还有另外一个值,即调用上下文,也就是this关键字的值
- 如果函数是在一个对象上被调用或通过一个对象被调用,这个对象就是函数的调用上下文或this值
- JavaScript函数可以嵌套定义在其他函数里,内嵌的函数可以访问定义在函数作用域的任何变量 ==> JavaScript函数是闭包,基于闭包可以实现重要且强大的编程技巧
定义函数
- 使用function关键字
- ES6定义了一种新的方式,可以不通过function关键字定义函数,使用箭头函数来定义函数
函数声明
function factorial(x) {
if(x <= 1) return 1
return x * factorial(x-1)
- 要理解函数声明,关键是理解函数的名字变成了一个变量,这个变量的值就是函数本身。
函数表达式
- 函数表达式看起来很像函数声明,但他们出现在复杂表达式或语句的上下文中,而且函数名是可选的
/* let stack = []
stack.push(1, 2)
s
console.log(stack) // [1, 2]
stack.pop()
console.log(stack) // [1] */
let q = []
let len = q.push(1, 2)
console.log(len) // 2
q.shift()
console.log(q) // [2]
let a = []
a.unshift(1)
a.unshift(2) // [2, 1]
a = []
a.unshift(1, 2) // [1, 2]
// 定义一个对参数求平方的函数,并将其赋值给一个变量
const square = function(x){ return x * x}
// 函数表达式也可以包含名字,这对递归有用
const f = function fact(x) { if (x <= 1) return 1; else return x * fact(x - 1)}
// 函数表达式也可以用作其他函数的参数
[3, 2, 1].sort(function(a, b){
return a-b
})
// 函数表达式也可以定义完以后立即调用
let tensquared = (function(x) { return x * x}(10))
// tensquared() // 报错
tensquared // 100 tensquared代表的就是整个函数
使用函数声明定义函数f() 与 创建一个函数表达式再将其赋值给变量f有一个重要的区别
- 声明形式:先创建好函数对象,然后再运行包含它们的代码,而且函数的定义会被提升到顶部,因此在定义函数的语句之前就可以调用它们。
- 定义为表达式的函数:这些函数在定义它们的表达式实际被求值之前是不存在的,且定义为表达式的函数不能在它们的定义之前调用!
箭头函数
- 使用“箭头”分隔函数的参数和函数体,因为箭头函数是表达式而不是语句,所以不用使用function关键字,而且也不需要函数名
- 箭头函数的一般形式
const sum = (x, y) => { return x + y; }
- 函数体只有一个return 语句的话,那么可以省略return 关键字、语句末尾的分号以及花括号,将函数体写成一个表达式,值将被返回
const sum = (x, y) => x + y
- 在进一步 如果箭头函数只有一个参数,也可以省略包围参数列表的圆括号
const polynomial = x => x*x + 2*x + 3
- 对于没有参数的箭头函数则必须把空圆括号写出来
const constantFunc = () => 42
重要
- 如果箭头函数的函数体是一个return 语句,但要返回的表达式是对象字面量,那必须要把这个对象字面量放在一对圆括号中,以避免解释器分不清花括号到底是函数体的花括号,还是对象字面量的花括号。
const f = x => {return { value : x;}} // 正 f() 返回一个对象
const g = x = > ({value : x}) // 正 g() 返回一个对象
箭头函数的简洁语法让他们非常适合作为值传给其他函数,在使用map() 、filter()和reduce() 等数组方法时非常常见
// 得到一个过滤掉null 元素的数组
let filtered = [1, null, 2, 3].filter(x => x !== null) // filtered == [1, 2, 3]
// 求数值的平方
let squares = [1, 2, 3, 4].map(x => x*x) // squares == [1, 4, 9, 16]
与其他方式定义的函数的区别
- 箭头函数从定义自己的环境中继承this关键字的值,而不是像以其他方式定义的函数那样定义自己的调用上下文
- 箭头函数没有prototype属性,这意味着箭头函数不能作为新类的构造函数
嵌套函数
- js中函数可以嵌套在其他函数中
function hypotenuse(a, b) {
function square(x) { return x * x;}
return Math.sqrt(square(a) + square(b));
}
- 关于嵌套函数,最重要的是理解它们的变量作用域规则:嵌套函数可以访问包含自己的函数(或更外层函数)的参数和变量
- 在上述函数中,内部函数square() 可以读写外部函数 hypotenuse() 定义的参数a和b
调用函数
5种方式来进行调用
- 作为函数
- 作为方法
- 作为构造函数
- 通过call() 或 apply() 方法间接调用
- 通过js语言特性隐式调用
1、函数调用
let total = distance(0, 0, 2, 1) + distance(2, 1, 3, 5)
2、方法调用
- 方法其实就是js中的函数,只不过他保存为对象的属性,如:有一个函数f和一个对象o,那么可以给o定义一个名为m的方法
o.m = f
- 对象o有了方法m() 后,就可以这样进行调用
o.m()
- 如果m期待两个参数,可以这样调用
o.m(x, y)
方法调用与函数调用有一个重要区别
- 调用上下文, 属性访问表达式有两部分构成,对象(这里的o)和属性名(m),在像这样的方法调用表达式中,对象o会成为调用上下文,而函数体可以通过关键字this引用这个对象
/* let stack = []
stack.push(1, 2)
console.log(stack) // [1, 2]
stack.pop()
console.log(stack) // [1] */
// let q = []
// let len = q.push(1, 2)
// console.log(len) // 2
// q.shift()
// console.log(q) // [2]
// let a = []
// a.unshift(1)
// a.unshift(2) // [2, 1]
// a = []
// a.unshift(1, 2) // [1, 2]
// // 定义一个对参数求平方的函数,并将其赋值给一个变量
// const square = function(x){ return x * x}
// // 函数表达式也可以包含名字,这对递归有用
// const f = function fact(x) { if (x <= 1) return 1; else return x * fact(x - 1)}
// // 函数表达式也可以用作其他函数的参数
// [3, 2, 1].sort(function(a, b){
// return a-b
// })
// // 函数表达式也可以定义完以后立即调用
// let tensquared = (function(x) { return x * x}(10))
// // tensquared() // 报错
// tensquared // 100 tensquared代表的就是整个函数
/* let a = function square(x) {
return x * x
} */
/* function hypotenuse(a, b) {
function square(x) { return x * x;}
return Math.sqrt(square(a) + square(b));
} */
let calculator = {
operand1 : 1,
operand2 : 1,
add() {
// 此处的this就是 包含了calculator等内容
/*
{ operand1: 1, operand2: 1, add: [Function: add] }
*/
console.log(this) // 其中此处的this就是函数调用的上下文
this.result = this.operand1 + this.operand2
console.log(this.result)
},
}
console.log(calculator.add())
console.log(calculator.result)
// 显示结果 非常关键
{ operand1: 1, operand2: 1, add: [Function: add] }
2
undefined
2
this关键字
- this是一个关键字,不是变量也不是属性名。JavaScript语法不允许给this赋值
- this关键字不具有变量那样的作用域机制,除了箭头函数,嵌套函数不会继承包含函数的this值
- 如果嵌套函数被当做方法来调用,那么它的this值就是调用它的对象。
- 如果嵌套函数(不是箭头函数)被当做函数来调用,那么this的值要么是全局对象(非严格模式),要么是undefined(严格模式)
- **常见错误:**对于定义在方法中的嵌套函数,如果将其当做函数来调用,自以为可以使用this获得这个方法的调用上下文
代码演示
let o = { // 对象o
m : function() { // 对象的方法m
let self = this // 把this的值保存在变量中
this === o // true this是对象o
f()
function f() { // 嵌套函数f
this === o // this是全局对象或undefined
self === o // true :self是外部的this的值
}
}
}
o.m() // 在对象o上调用方法m
注意
- 在嵌套函数f() 内部,this关键字不等于对象o,这被广泛认为是JavaScript语言的一个缺陷
- 上面的代码演示了一个常见的技巧:在方法m中,我们把this值赋值给变量self,然后在嵌套函数f中,就可以使用self而非this来引用包含对象
- 在ES6及之后的版本,解决这个问题的另一个技巧是把嵌套函数f转换为箭头函数,因为箭头函数可以继承this的值
const f = ()=> {
this === o // true 因为箭头函数继承this
}
函数表达式不像函数声明语句那样会被提升,因此为了让上面的代码有效,需要将这个函数f的定义放到方法m中调用函数f的代码之前
- 还有一个技巧是:调用嵌套函数的bind() 方法,以定义一个在指定对象上被隐式调用的新函数
const f = (function() {
this === o // true 因为我们把这个函数绑定到了外部的this中
}).bind(this)
构造函数调用
- 如果函数或方法调用前面加了一个关键字new,那它就是构造函数调用
- 如果构造函数调用在圆括号中包含参数列表,则其中的参数表达式会被求值,并以与函数和方法调用相同的方式传给函数
- 没有参数列表,构造函数调用时其实也可以省略空圆括号
o = new Object()
// 和上面的代码是等价的
o = new Object
间接调用
- call() 和 apply() 两个方法可以用来间接调用函数,这两个方法允许我们指定调用时的this值,这意味着可以将任意函数作为任意对象的方法来调用,即使这个函数实际上并不是该对象的方法
- call() 方法使用自己的参数列表作为函数的参数,apply()则期待数组作为参数
可选形参与默认值
- 当调用函数时传入的实参少于声明的形参时,额外的形参会获得默认值,通常是undefined
if(a === undefined) a = [ ] // 如果是undefined,创建一个新数组
// 等价于
a = a || [ ]
|| 操作符 : 在第一个参数是真值的时候,返回第一个参数,否则返回第二个参数
- 在设置有可选参数的函数时,一定要把可选参数放在参数列表最后,这样在调用时才可以省略
在ES6及更高的版本中,可以在函数形参列表中直接为每个参数定义默认值。语法是在形参名后面加上等于号和默认值,这样在没有给该形参传值时就会使用这个默认值
/*
把对象o的可枚举属性名放到数组a中,返回a
如果不传a 则创建一个数组。
*/
function getPropertyNames(o, a=[]) {
for(let property in o) a.push(property)
return a
}
函数的形参默认值表达式会在函数调用时求值,不会在定义时求值。因此每次调用getPropertyNames() 函数时如果只传一个参数,都创建并传入一个新的空数组
- 也可以使用变量或函数调用计算形参的默认值,对此有一种有意思的情形就是,如果函数有多个形参,则可以使用前面参数的值来定义后面参数的默认值
/*
这个函数返回一个表示矩形尺寸的对象
如果值提供width,则height是它的两倍
*/
const rectangle = (width, height=width*2) => ({width, height})
console.log(rectangle(1)) // { width: 1, height: 2 }
剩余形参与可变长度实参列表
- 形参默认值让我们编写少于形参个数的实参来调用函数
- 剩余形参的作用恰好相反:让我们能够编写在调用时传入比形参多任意数量的实参的函数
- 举例:示例函数:接收一个或多个实参,返回其中最大的一个:
function max(first=-Infinity, ...rest) {
let maxValue = first // 假设第一个参数是最大的
// 遍历其他参数,寻找更大的数值
for(let n of rest) {
if(n > maxValue) {
maxValue = n
}
}
// 返回最大的数值
return maxValue
}
console.log(max(1, 10, 2, 3, 4)) // 10
Arguments(避免使用)
- 剩余形参是ES6引入JavaScript
- ES6之前,变长函数是基于Arguments对象实现的,也就是说,在任何函数体内,标识符arguments引用该次调用的Arguments对象
- Arguments对象是一个类数组对象,它允许通过数值而非名字取得传给函数的参数值
- 举例:之前展示的max() 函数 使用Arguments对象重写了一下,没有使用剩余参数
function max(x) {
let maxValue = -Infinity
console.log(arguments) // [Arguments] { '0': 1, '1': 10, '2': 2, '3': 3, '4': 4 }
// 遍历arguments,查找并记住最大的数值
for(let i = 0; i < arguments.length; i++) {
if(arguments[i] > maxValue) maxValue = arguments[i]
}
// 返回最大的数值
return maxValue
}
console.log(max(1, 10, 2, 3, 4)) // 10
新写的代码中应该避免使用它,在重构老代码时,如果碰到了使用arguments的函数,通常可以将其替换为…args剩余形参
在函数调用中使用扩展操作符
- 在期待单个值的上下文中,扩展操作符…用于展开或“扩展”数组(或任何可迭代对象,如字符串)的元素
- 扩展操作符同样可以用于函数调用中
let numbers = [5, 2, 10, -1, 9, 100, 1]
Math.min(...numbers) // -1
- 如果在函数定义(而非函数调用)时使用同样的…语法,那么会产生与扩展操作符相反的作用
- 在函数定义中使用 … 可以将多个函数实参收集到一个数组中,剩余形参和扩展操作符经常同时出现
- 例:以下函数,它接收一个函数实参并返回该函数的可测量版本
// 这个函数接收一个函数并返回一个包装后的版本
function timed(f) {
return function (...args) {// 把实参收集到一个剩余形参数组args中
console.log(`Entering function ${f.name}`) // Entering function benchmark
let startTime = Date.now()
try {
// 把收集到的实参传給包装后的函数
return f(...args) // 把args扩展回原来的形式
} finally {
// 在返回被包装的返回值之前,打印经过的时间
console.log(`Exiting ${f.name} after ${Date.now() - startTime} ms`) // Exiting benchmark after 4 ms
}
}
}
// 以简单粗暴的方式计算1到n的数值之和
function benchmark(n) {
let sum = 0
for(let i = 0; i <= n; i++) {
sum += i
}
return sum
}
// 调用测试函数的计时版
/*
为啥是timed(benchmark)(1000000) 这么进行调用呢
因为timed函数中 在try中使用了 return f(...args) 将f函数解构返回了
*/
console.log(timed(benchmark)(1000000)) // 500000500000 数值之和
把函数实参解构为形参
- 调用函数时如果传入一个实参列表,则所有参数值都会被赋给函数定义时声明的形参,函数调用的这个初始化阶段非常类似于变量赋值
- 如果我们定义了一个函数,它的形参名包含在方括号中,那说明这个函数期待对每对方括号都传入一个数组值
- 作为调用过程的一部分,我们传入的数组实参会被解构赋值为单独的命名形参
- 例如:假设要用数组表示两个数值的2D向量,数组的第一个元素是X坐标,第二个元素是Y坐标,可表示成如下
function vectorAdd(v1, v2) {
return [v1[0]+v2[0], v1[1]+v2[1]]
}
vectorAdd([1, 2], [3, 4]) // [4, 6]
如果换成把两个向量实参解构为命名更加清晰的实参,上述代码会更好理解
function vectorAdd([x1, y1], [x2, y2]) {
return [x1 + x2, y1 + y2]
}
vectorAdd([1, 2], [3, 4]) // [4, 6]
- 类似的,如果定义的函数需要一个对象实参,也可以把传入的对象解构赋值给形参
/* 用标量乘以向量{x, y} */
function vectorMultiply({ x, y }, scalar) {
return { x: x * scalar, y: y * scalar }
}
vectorMultiply({ x: 1, y: 2 }, 2) // {x:2, y:4}
- 这个例子把一个对象实参解构为两个形参,由于形参名与对象的属性名一致,所以相当清晰,但是如果需要把解构的属性赋值给不同的名字 ,那代码会更长也更不好理解
function vectorAdd(
{x:x1, y:y1}, // 把第一个对象展开为x1和y1
{x:x2, y:y2} // 把第二个对象展开为x2和y2
){
return {x:x1+x2, y:y1+y2}
}
vectorAdd({x:1, y:2}, {x:3, y:4}) // {x:4, y:6}
- 对于像{x:x1, y:y1},这样的解构语法,关键是记住na
参数类型
- JavaScript方法的参数没有预定义的类型,在调用传参时也没有类型检查
- 可以用描述性强的名字作为函数参数,同时通过在注释中解释函数的参数来解决这个问题
- JavaScript会按需执行任意的类型转换,因此如果你的函数接收字符串参数,而调用时传入的是其他类型的值,则这个值在函数想把它当成字符串使用时,会尝试将它转换为字符串
- 所有原始类型的值都可以转换为字符串,所有对象都有toString() 方法
- 不过也有例外,以arraycopy() 为例,该方法期待一个或多个数组参数,如果参数类型不对就会失败,除非你书写的是一个私有函数(只会在自己代码内部调用,否则就必须增加像下面用于检查参数类型的代码)
/*
返回可迭代对象a中所有元素之和
a的元素必须全部都是数值
*/
function sum(a) {
let total = 0
for(let element of a) { //如果a不是可迭代对象,则抛出TypeError
if(typeof element !== 'number') {
throw new TypeError("sum(): elements must be numbers")
}
total += element
}
return total
}
console.log(sum([1, 2, 3])) // 6
console.log(sum(1, 2, 3)) // !typeError :1 不是可迭代对象
console.log(sum([1, 2, '3'])) // !typeError:元素必须是数值
函数作为值
- 函数最重要的作用特性在于可以定义和调用它们
- 在JavaScript中,函数不仅是语法,也是值
理解函数即是JavaScript数据又是JavaScript语法到底意味着什么?
例子:
function square(x) { return x * x }
- 这个定义创建了一个新函数对象并把它赋值给变量square 函数的名字并不重要,因为它就是引用函数对象的一个变量名,把函数赋值给另一个变量同样可以调用
let s = square
square(4) // 16
s(4) // 16
- 除了变量,也可以把函数赋值给对象的属性,如前所述,这个时候将函数称为 方法
let o = {square: function(x) {return x*x }} // 对象字面量
let y = o.square(16) // y == 256
- 函数甚至可以没有名字,比如可以把匿名函数作为一个数组元素
let a = [x => x * x, 20]
a[0](a[1]) // 400
- 为了更好地理解函数作为值有多大用处,可以想一想Array.sort() 方法,这个方法可以对数组元素进行排序
- 排序方法有很多种,sort() 方法 可选择性的接收一个函数作为参数,并根据这个函数的返回值决定如何排序,这个函数让Array.sort()变得非常通用
函数当做值可以做什么事情呢 ?
/*
函数作为值
定义几个简单的函数
*/
function add(x, y) {return x + y}
function subtract(x, y) {return x - y}
function multiply(x, y) {return x * y}
function divide(x, y) {return x / y}
/*
这个函数接收前面定义的任意一个函数
作为参数 然后在用两个操作数调用它
*/
function operate(operator, operand1, operand2) {
return operator(operand1, operand2)
}
/* 可以像如此调用这个函数,计算(2+3) + (4*5)的值 */
let i = operate(add, operate(add, 2, 3), operate(multiply, 4, 5))
/*
为了演示方便
我们又一次实现这些简单的函数
*/
const operators = {
add: (x, y) => x + y,
subtract: (x, y) => x - y,
multiply: (x, y) => x * y,
divide: (x, y) => x / y,
pow: Math.pow // 预定义的函数,没有任何关系
}
/*
这个函数只接收操作的名字,然后在对象中查询
这个名字 然后再使用传入的操作数调用它
注意这里调用函数的语法
*/
function operate2(operation, operand1, operand2) {
if(typeof operators[operation] === 'function') {
return operators[operation](operand1, operand2)
}else throw "unknown operator"
}
operate2('add', 'hello', operate2('add', " ", "world")) // "hello world"
operate2("pow", 10, 2) // 100
定义自己的函数属性
- 函数在JavaScript中并不是原始值,而是一种特殊的对象,这意味着函数也可以有属性
- 如果函数需要一个“静态”变量,且这个变量的值需要在函数每次调用时都能访问到,则通常把这个变量定义为函数自身的一个属性
- 假如:我要写一个每次调用都返回唯一整数的函数,那么每次调用都不能返回相同的值,为了保证这一点,函数需要记录自己已经返回过的值,这个信息必须在每次调用时都能访问到
- 可以把这个信息保存在一个全局变量中,但没必要,因为这个信息只有这个函数自己会用到,更好的方式是把这个信息保存在函数对象的一个属性中
例子1:每次调用都返回唯一整数值的函数实现
/*
初始化函数对象的计数器(counter)属性
函数声明会提升,因此我们可以在函数声明之前在这里给它赋值
*/
uniqueInteger.counter = 0
/*
这个函数每次调用时都返回一个不同的整数
它使用自身的属性记住下一个要返回什么值
*/
function uniqueInteger() {
return uniqueInteger.counter++
}
console.log(uniqueInteger()) // 0
console.log(uniqueInteger()) // 1
例子2:下面的factorial() 函数使用了自身的属性来缓存之前计算的结果(函数将自身作为一个数组)
/*
计算阶乘并把结果缓存到函数本身的属性中
*/
function factorial(n) {
if(Number.isInteger(n) && n > 0) {
if(!(n in factorial)) { // 如果没有缓存结果
factorial[n] = n * factorial(n-1) // 计算并缓存这个结果
}
return factorial[n] // 返回缓存的结果
}else {
return NaN
}
}
factorial[1] = 1 // 初始化缓存值 保存最基础的值
console.log(factorial(6)) // 720
console.log(factorial[5]) // 120
函数作为命名空间
- 在函数体内声明的变量在函数外部不可见
- 因此有时候,可以把函数用作临时的命名空间,这样可以保证在其中定义的变量不会污染全局命名空间
- 假设有一段JavaScript代码,想要在几个不同的JavaScript程序中使用它,问题:(我们不知道哪些程序创建的变量会不会跟这段代码中的变量发生冲突) 解决方案:把这段代码放到一个函数中,然后调用这个函数,这样原本可能定义在全局的变量,就变成了函数的局部变量
function chunkNamespace() {
// 要复用的代码放在这里
// 在这里定义的任何变量都是函数的局部变量
// 不会污染全局命名空间
}
chunkNamespace() // 调用该函数
- 以上代码只定义了一个全局变量,即函数chunkNamespace。定义一个函数都嫌多的话,可以在一个表达式中定义并调用匿名函数
(function() { // 将chunkNamespace() 函数重写为一个无名表达式
// 要复用的代码写在这里
}()) // 函数定义结束后立即调用它
- 在一个表达式中定义并调用匿名函数的技术非常有用,–》 立即调用函数表达式 注意书写过程中括号的使用
函数属性、方法与构造函数
- 函数在JavaScript中也是一种值,使用typeof操作符返回“function”
- 函数实际上是一种特殊的JavaScript对象,由于函数是对象,因此它们也有属性和方法
length属性
- 函数有一个只读的length属性,表示函数的元数,即函数在参数列表中声明的形参个数
- 这个值通常表示调用函数时应该传入的参数个数
name属性
- 函数有一个只读的name属性,表示定义函数时使用的名字(用名字定义的),如果是未命名的函数,表示在第一次创建这个函数时赋给该函数的变量名或属性名
prototype属性
- 除了箭头函数,所有函数都有一个prototype属性,这个属性引用一个被称为原型对象的对象
- 每个函数都有自己的原型对象
- 当函数作为构造函数使用时,新创建的对象从这个原型对象继承属性
call() 和 apply() 方法
- call() 和 apply() 允许间接调用一个函数,就像这个函数是某个其他对象的方法一样
- call() 和 apply() 第一个参数都是要在其上调用这个函数的对象,也就是函数的调用上下文,在函数体内它会变成this关键字的值
- 要把函数f() 作为对象o的方法进行调用(不传参数),可以使用call() 和 apply()
f.call(o)
f.apply(o)
- 上面两行代码类似于下面代码(假设o并没有属性m)
o.m = f // 把f作为o的一个临时方法
o.m()
delete o.m
- 箭头函数从定义它的上下文中继承this的值,这个this值不能通过call() 和 apply() 重写,如果对箭头函数调用这两个方法,那么第一个参数实际上会被忽略
- 除了作为调用上下文传给call() 的第一参数 后续的所有参数都会传给被调用的函数
- 比如:要将函数f() 作为对象o 的方法进行调用,并同时给函数f() 传两个参数,写法
f.call(o, 1, 2)
- apply()和call() 类似,只不过传给函数的参数需要以数组的形式提供
f.apply(o, [1, 2])
bind() 方法
- bind()方法的主要目的就是将函数绑定到对象上
- 如果在函数f上调用bind() 方法并传入对象o,则这个方法会返回一个新函数。如果作为函数来调用这个新函数,就会像f是o的方法一样调用原始函数
function f(y) { return this.x + y } // 这个函数需要绑定
let o = { x : 1 } // 要绑定的对象
let g = f.bind(o) // 调用g(x) 会在o上调用f()
g(2) // 3
let p = { x:10, g } // 作为这个对象的方法调用g()
p.g(2) // 3 : g仍然绑定到o, 而非p
- 箭头函数从定义它们的环境中继承this值, 且这个值不能被·bind() 覆盖, 因此前面代码中的函数f() 如果是用箭头函数定义的,则绑定不会起作用
- 调用bind() 最常见的目的就是让非箭头函数变得像箭头函数
除了把函数绑定到对象,bind() 方法还会做其它事,bind() 也可以执行“部分应用” - 即在第一个参数之后传给bind() 的参数也会随着this值一起被绑定
let sum = (x, y) => x + y // 返回2个参数之和
let succ = sum.bind(null, 1) // 把第一个参数绑定为1
succ(2) // 3 x绑定到1,2会传给参数y
function f(y, z) { return this.x + y + z }
let g = f.bind({x:1}, 2) // 绑定this和y
g(3) // 6 this.x 绑定到1 y绑定到2 z是3
toString() 方法
- 与所有JavaScript对象一样,函数也有toString() 方法
- ECMAScript规范要求这个方法返回一个符合函数声明语句的字符串
Function构造函数
- 因为函数是对象,所有就有一个Function() 构造函数可以用来创建新函数
const f = new Function('x', 'y', "return x*y; ")
- 这行代码创建了一个新函数,差不多相当于使用如下语法定义的函数
const f = function(x, y) { return x * y }
- Function() 构造函数可以接收任意多个字符串参数,其中最后一个参数是函数体的文本
- Function() 非常重要的一点:它创建的函数不使用语法作用域,而是始终编译为顶级函数一样
let scope = 'global'
function constructFunction() {
let scope = 'local'
return new Function("return scope") // 不会捕获局部作用域
}
/*
返回'global'
因为Function() 构造函数返回的函数不使用局部作用域
*/
constructFunction()() // "global"
函数式编程
- JavaScript可以把函数作为对象来操作意味着JavaScript中使用函数式编程技巧
- 像Map() 和 Reduce() 这样的数组方法就特别适合函数式编程风格
使用函数处理数组
例子:有一个数值数组,希望计算这些数值的平均值和标准差
- ① 使用非函数式风格的代码
/*
计算数值的平均值和标准差
*/
let data = [1, 1, 3, 5, 5]
// Jsuan1
let total = 0
for(let i = 0; i < data.length; i++) {
total += data[i]
}
let mean = total / data.length
// 计算标准差,首先要计算每个元素相对于平均值偏差的平方
total = 0
for(let i = 0; i < data.length; i++) {
let deviation = data[i] - mean
total += deviation * deviation
}
let stddev = Math.sqrt(total / (data.length - 1))
- 而使用数组方法map() 和 reduce() 可以像下面这样以简洁的函数式风格实现同样的计算
// 首先先定义两个简单的函数
const sum = (x, y) => x + y
const square = x => x * x
// 然后,使用数组方法计算平均值和标准差
let data = [1, 1, 3, 5, 5]
let mean = data.reduce(sum) / data.length
console.log(mean) // 3
let deviation = data.map(x => x - mean)
console.log(deviation) // [ -2, -2, 0, 2, 2 ]
console.log(deviation.map(square)) // [ 4, 4, 0, 4, 4 ]
console.log(deviation.map(square).reduce(sum)) // 16
let stddev = Math.sqrt(deviation.map(square).reduce(sum) / (data.length - 1))
- 新版本的代码看起来与第一个版本差别很大,但仍然调用对象上的方法,因此还可以看出一些面向对象的痕迹,定义map() 和 reduce() 方法的函数版
const map = function(a, ...args) {return a.map(...args) }
const reduce = function(a, ...args) {return a.reduce(...args) }
// 定义了map() 和 reduce() 函数后,计算平均值和标准差的代码
const sum = (x, y) => x + y
const square = x => x * x
let data = [1, 1, 3, 5, 5]
let mean = reduce(data, sum) / data.length
let deviation = map(data, x => x - mean)
let stddev = Math.sqrt(reduce(map(deviation, square), sum) / (data.length - 1))
高阶函数
高阶函数就是操作函数的函数 - 它接收一个或多个函数作为参数并返回一个新函数,例如:
/*
高阶函数返回一个新函数
新函数把参数传给f并返回f返回值的逻辑非
*/
function not(f) {
return function(...args) { // 返回一个新函数
let result = f.apply(this, args) // 新函数调用f
return !result
}
}
应用
const even = x => x % 2 === 0 // 确定数值是不是偶数的函数
const odd = not(even) // 确定数值是不是奇数的新函数
[1, 1, 3, 5, 5].every(odd) // true
- 这个not()函数就是一个高阶函数, 因为它接收一个函数参数并返回一个新函数
- 再比如下面的mapper() 函数**,这个函数接收一个函数参数并返回一个新函数,新函数使用传入的函数把一个数组映射为另一个数组**
- 这个函数使用了前面定义的map()函数
/*
返回一个函数,这个函数接收一个数组
并对每个元素应用f,返回每个返回值的数组
*/
function mapper(f) {
return a => map(a, f)
}
const increment = x => x+1
const incrementAll = mapper(increment)
console.log(incrementAll([1, 2, 3]))
高阶函数,接收两个函数f和g,返回一个计算f(g())的新函数
最后
以上就是深情小鸽子为你收集整理的JavaScript权威指南(原书第7版) 犀牛书第3章 语法结构第4章 表达式与操作符第5章 语句跳转语句第6章 对象第7章 数组数组方法第8章 函数的全部内容,希望文章能够帮你解决JavaScript权威指南(原书第7版) 犀牛书第3章 语法结构第4章 表达式与操作符第5章 语句跳转语句第6章 对象第7章 数组数组方法第8章 函数所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复