发布日期:Jun 22, 2023 更新日期: Jul 20, 2023文章字数 0阅读时长:0分钟

type
Post
status
Published
date
Jun 22, 2023
slug
JavaScript
summary
tags
前端
category
技术分享
icon
password

第三章 语言基础

3.3 变量

3.3.1 var声明

使用var操作符定义的变量会成为包含它的函数的局部变量。 在函数内定义变量时省略var操作符,可以创建全局变量。
“提升” (hoist), 也就是把所有变量声明都拉到函数作用域的顶部。
反复多次使用var声明同一个变量也没问题。
for循环定义的迭代变量会渗透到循环体外部。

3.3.2 let声明

声明的范围是块作用域,不允许同一个块作用域中出现冗余申明。
在let声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone), 在此阶段引用任何后面才声明的变量都会抛出ReferenceError。

3.3.3 const声明

const 的行为与let基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改const声明的变量会导致运行时错误。
const 声明的限制只适用于它指向的变量的引用。换句话说,如果const变量引用的是一个对象,那么修改这个对象内部的属性并不违反const的限制

3.4 数据类型

3.4.2 undefind类型

当使用var或let声明了变量但没有初始化时,相当于给变量赋予了undefind值。

3.4.3 Null类型

typeof null会返回“object”。这是因为特殊值null被认为是一个对空对象的引用。
null值表示一个空对象指针。
undefind值是由null值派生而来的,他们表面上相等。

3.4.4 Boolean类型

数据类型
转换为true的值
转换为false的值
Boolean
true
false
String
非空字符串
“” (空字符串)
Number
非零数值(包括无穷值)
0、NaN(后面会介绍)
Object
任意对象
null
Undefind
N/A(不存在)
undefind

3.4.5 Number类型

3.NaN
NaN意思是“不是数值”(Not a Number),用于表示本来要返回数值的操作失败了。
NaN有几个独特的属性,首先,任何涉及NaN的操作始终返回NaN。
其次,NaN不等于包括NaN在内的任何值。
4.数值转换
有3个函数可以将非数值转换为数值:Number()、parseInt()parseFloat()
Number()函数基于如下规则执行转换。
  • 布尔值,true转换为1,false转换为0
  • 数值,直接返回
  • null,返回0
  • undefind,返回NaN
  • 字符串,应用如下规则
  • 如果字符串包含数值字符,包括数值字符前面带加减号的情况,则转换为一个十进制数值。如果字符串包含有效的浮点值格式如“1.1”,则会转换为相应的浮点值(同样,忽略前面的零)。如果字符串包含有效的十六进制格式如“0xf”,则会转换为与该十六进制值对应的十进制整数值。如果是空字符串(不包含字符),则返回0。如果字符串包含上述情况之外的其他字符,则返回NaN。
  • 对象,调用valueOf()方法,并按照上述规则转换返回的值。如果转换结果是NaN,则调用toString()方法,再按照转换字符串的规则转换。
parseInt():空字符串也会返回NaN
当parseInt()遇到第二个小数点时,剩余字符都会被忽略

3.4.6 String类型

ECMAScript中的字符串是不可变的(immutable),意思是一旦创建,它们的值就不能变了。要修改某个变量中的字符串值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量。

3.5 操作符

3.5.6 加性操作符

1. 加法操作符
加法操作符(+)用于求两个数的和。
如果两个操作数都是数值,加法操作符执行加法运算并根据如下规则返回结果。
  • 如果有任一操作数是NaN,则返回NaN
  • 如果是Infinity加Infinity,返回Infinity
  • 如果是-Infinity加-Infinity,返回-Infinity
  • 如果是Infinity加-Infinity,返回NaN
  • 如果是+0 加 +0,返回+0
  • 如果是-0 加 +0,返回+0
  • 如果是-0 加 -0,返回-0
不过,如果有一个操作数是字符串,则要应用如下规则:
  • 如果两个操作数都是字符串,则拼接
  • 如果只有一个是字符串,则将另一个操作数转换为字符串,再拼接。
如果有任一操作数是对象、数值或布尔值,则调用它们的toString()方法以获取字符串,对于undefind和null则调用String()函数,分别获取“undefind"和”null“
2. 减法操作符
简单记忆,减法操作符会尽力将两个操作数转成数值(Number()。会优先调用对象的valueOf()方法,如果没有valueOf()则调用toString()方法,再将获得的值变为数值。如果操作数存在NaN,返回NaN
let result1 = 5 - true // true被转换成1, 结果是4 let result2 = NaN - 1 // NaN let result3 = 5 - '' // ''被转换成0, 结果是5 let result4 = 5 - null // null被转换成0,结果是5

3.5.8 相等操作符

1. 等于和不等于(== , !=)
等于和不等于在比较时会进行类型转换。
简单记忆,如果两个操作数不都是字符串,则尽力将两边的操作符转为数字。null只和null或undefind相等,undefind只和undefind或null相等。NaN不和任何值相等。
true == 1 // true, 因为会把true转为1 NaN == NaN // false, NaN不和任何值相等 "5" == 5 // true, 会把"5"转为5
2. 全等和不全等(===, !==)
全等和不全等在比较时会首先判断数据类型是否相同,不相同则返回false。但是NaN仍然不和NaN相等。

第四章:变量、作用域与内存

4.1 原始值与引用值

ECMAScript包含两种不同类型的数据:原始值和引用值。原始值就是最简单的数据,引用值则是由多个值构成的对象。
保存原始值的变量按值访问,操作的是储存在变量中的实际值
保存引用值的变量是按引用访问的,操作的是对该对象的引用而不是对象本身

4.1.2 复制值

在把值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在位置。
引用值和原始值的区别在于,引用值复制的值实际上是一个指针,指向堆内存中的对象
所以对同一个指针所指向的对象进行操作,变化会在两个变量中显示出来。

4.1.3 传递参数

ECMAScript中所有函数的参数都是按值传递的
function addTen(num){ num += 10 return num } let count = 20 let result = addTen(count) console.log(count) // 20,没有变化 console.log(result) // 30
num实际上是一个局部变量,这个num保存了实参的值,仅仅是一个副本,因此不会影响外部变量。
function setName(obj){ obj.name = "ryan" } let person = new Object() setName(person) console.log(person.name) // "ryan"
这里的person是一个引用值,obj参数依然遵循按值传递的规则,但是obj是一个指针副本,因此对指针所指向的对象进行修改,实际上是通过引用进行修改,变化会表现在外部。
function setName(obj){ obj.name = "ryan" obj = new Object() obj.name = "Titan" } let person = new Object() setName(person) console.log(person.name) // "ryan"
现在setName()函数内部会将obj变量重新赋值一个新的对象,但是这个变化不会反应在外部。原因就是,obj实际上只是一个指针副本,覆盖这个指针并不会影响这个指针所指向的对象。

4.1.4 确定类型

typeof无法区分null或对象。typeof虽然对原始值很有用,但它对引用值的作用不大。
为解决这个问题,ECMAScript提供了instanceof操作符,语法如下:
result = variable instanceof constructor
所有引用值都是Object的实例,所以通过instanceof操作符检测任何引用值和Object构造函数都会返回true。
如果变量是给定构造函数的实例,则instanceof操作符返回true。

4.2 执行上下文与作用域

执行上下文(又称作用域)(以下简称“上下文”)的概念在JavaScript中颇为重要。每个上下文都有一个关联的变量对象(variable object)。这个变量对象保存了上下文中所有变量和函数。
全局上下文即是最外层的上下文。浏览器中,全局上下文就是window对象。
每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。函数执行结束,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。
上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。代码正在执行的上下文的变量对象始终位于作用域的最前端。
如果上下文是函数,则其活动对象(activation object)用作变量对象。活动对象最初只有一个定义变量:arguments。作用域链中的下一个变量对象来自包含上下文(上层上下文),以此类推至全局上下文。全局上下文的变量对象始终是最后一个变量对象。

4.3 垃圾回收

JavaScript通过自动内存管理实现内存分配和闲置资源回收。基本思路很简单:确定哪个变量不会再使用,然后释放它占用的内存。
在浏览器的发展史上,用到过两种主要的标记策略:标记清理引用计数

4.3.1 标记清理

标记清理(mark-and-sweep)是JavaScript最常用的垃圾回收策略。
垃圾回收程序在运行的时候会给存储在内存中的所有变量都加上标记(标记方法有很多种)。然后,它会去掉上下文中的变量以及被上下文的变量引用的变量的标记。而在此之后仍有标记的变量就是待删除的了,原因是任何在上下文中的变量已经无法访问到这些它们了。

4.3.2 引用计数

另一种没那么常用的垃圾回收策略是引用计数(reference counting)。其思路是对每个值都记录它被引用的次数。将一个引用值赋给一个变量时,将这个值的引用数加一。当这个变量被其他值覆盖时,这个值的引用数减一。当一个值的引用数为0时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。
引用计数在遇到循环引用时不适用,所谓循环引用,就是对象A有一个指针指向对象B,而对象B也引用了对象A。比如:
function problem(){ let objectA = new Object() // 这里的new Object()值我们叫它a, a的引用数加一 let objectB = new Object() // 这里的new Object()值我们叫它b, b的引用数加一 objectA.b = objectB // a的引用数再加一 objectB.a - objectA // b的引用数再加一 }
这里两个对象的引用数都为2,所以永远不会被回收。
由于循环引用问题的存在,Netscape在4.0版放弃了引用计数,转而采用标记清理。

4.3.4 内存管理

分配给浏览器的内存通常比分配给桌面软件的要少很多,因此减少内存占用十分重要。将内存占用量保持在一个较小的值可以让页面性能更好。优化内存占用的最佳手段就是保住在执行代码时只保持必要的数据。
如果数据不再必要,那么把它设置为null,从而释放引用。
let globalPerson = new Object() globalPerson = null // 解除globalPerson对值的引用

第五章:基本引用类型

Javascript中的对象称为引用值。有几种内置的引用类型可用于创建特定类型的对象。

5.1 Date

要创建日期对象,使用new操作符来调用Date构造函数:
let now = new Date()
Date类型将日期保存为自1970年1月1日午夜(零时)至今所经过的毫秒数。
在不给Date构造函数传参的情况下,创建的对象将保存当前的日期和时间。
let now = new Date() now.getTime() // 1602464952410(相当于Date.now())

5.1.1 继承的方法

Date类型重写了toLocaleString()**,toString()valueOf()**。
  • *toLocaleString()**方法返回与浏览器运行的本地环境一致的日期和时间。
  • *toString()**方法通常返回带时区信息的日期和时间。
  • *valueOf()**返回日期的毫秒表示。
let now = new Date() now.toLocaleString() // "2020/10/12 上午9:14:52" now.toString() // Mon Oct 12 2020 09:14:52 GMT+0800 (中国标准时间)" now.valueOf() // 1602465292914
常用方法
let now = new Date() now.getDate() // 12 (返回日期中的日(1~31)) now.getDay() // 1 (表示周几,0表示周日,6表示周六) now.getMonth() // 9 (返回日期中的月,0表示一月,11表示十二月) now.getHours() // 9 (返回日期中的时(0~23))

5.2 RegExp

RegExp类型是ECMAScript支持正则表达式的接口,提供了大多数基础的和部分高级的正则表达式功能。
正则表达式使用类似Perl的简洁语法来创建:
let expression = /pattern/flags
pattern(模式)可以是任何简单或复杂的正则表达式,包括字符类、限定符、分组、向前查找和反向引用。
每个正则表达式可以带零个或多个flags(标记),用于控制正则表达式的行为:
  • g: 全局模式,表示查找字符串的全部内容,而不是找到第一个就结束。
  • i: 不区分大小写,表示在查找匹配时忽略pattern的字符串大小写。
  • m: 多行模式,表示查找到一行文本末尾时会继续查找。
  • y: 粘附模式,表示只查找从lastIndex开始及之后的字符串。
使用不同模式和标记可以创建出各种表达式:
// 匹配字符串中的所有"at" let pattern1 = /at/g// 匹配第一个"bat"或"cat",忽略大小写 let pattern2 = /[bc]at/i// 匹配所有以"at"结尾的三字符组合,忽略大小写 let pattern3 = /.at/gi
如果要匹配元字符 ( [ { \ ^ $ | ] } ? * + . ,需要转义:
// 匹配第一个"[bc]at",忽略大小写 let pattern = /\[bc\at]/i// 匹配所有".at",忽略大小写 let pattern4 = /\.at/gi
RegExp实例的主要方法是exec()。主要用于配合捕获组的使用。这个方法只接收一个参数,即要应用模式的字符串。
let text = "attack on ryan" let pattern = /at(t)ack/let matches = pattern.exec(text) // ["attack", "t", index: 0, input: "attack on ryan", groups: undefined]
另一个常用方法是test()。接收一个字符串参数,如果能够匹配则返回true否则返回false。
let text = "attack on ryan" let pattern = /attack/ pattern.test(text) // true

5.3 原始值包装类型

为了方便操作原始值,ECMAScript提供了3中特殊的引用类型:Boolean、Number和 String。
每当用到某个原始值的方法或属性时,后台都会创建一个相应原始包装类型的对象,从而暴露出操作原始值的各种方法
let s1 = "some text" let s2 = s1.substring(2)
s1为基本字符串类型,本不应该存在**substring()**方法,但是后台会进行如下3个步骤:
  1. 创建一个String类型的实例
  1. 调用实例上的特定方法
  1. 销毁实例
可以想象成如下代码
let s1 = new String("some text") let s2 = s1.substring(2) s1 = null
因此给原始值添加属性或方法时无效的
let s1 = "some text" s1.color = "cyan" // (临时创建的实例会在语句结束后销毁) console.log(s1.color) // undefined

5.4 单例内置对象

ECMA-262对内置对象的定义是“任何由ECMAScript实现提供、与宿主环境无关,并在ECMAScript程序开始执行时就存在的对象”。这意味着开发者不用显式实例化内置对象,因为它们已经实例化好了。

5.4.1 Global

Global对象时ECMAScript中最特别的对象,因为代码不会显式访问它。ECMA-262规定Global对象为兜底对象,在全局作用域中定义的变量和函数都会变成Global对象的属性。
window对象
浏览器将window对象实现为Global对象的代理,因此所以全局作用域中声明的变量和函数都会变成window的属性。
var color = "red" window.color = "red"

5.4.2 Math

ECMAScript提供了Math对象作为保存数学公式、信息和计算的地方。
常用属性和方法
Math.E // 自然对数的基数e的值 Math.PI // Π的值 Math.min(2,5,1,10) // 返回一组数值中的最小值 Math.max(2,5,1,10) // 返回一组数值中的最大值 Math.ceil() // 向上舍入为最接近的整数 Math.floor() // 向下舍入为最接近的整数 Math.round() // 四舍五入返回整数 Math.random() // 返回[0, 1)之间的随机数

第六章:集合引用类型

6.1 Object

创建Object有两种方式,一种是使用new操作符和Object构造函数,另一种方式是使用对象字面量表示法(object literal)
let person = new Object() person.name = 'Ryan' // 对象字面量表示 let person2 = { name: 'Titan' }
字符串的属性名可以是字符串或数值,数值属性会自动转换为字符串
let person = { "name": "Ryan", age: 20, 5: true }
使用中括号可以通过变量访问属性,或设置属性。
let option = 'name' let person = { [option]: 'Ryan' } person[option] = 'AttackonRyan'
通常,点语法是首选的属性存取方式,除非访问属性时必须使用变量。

6.2 Array

ECMAScript中的数组是一组有序的数据,且每个槽位可以存储任意类型的数据。
ECMAScript中的数组也是动态大小的,会随着数据的添加而自动增长。

6.2.1 创建数组

有几种基本方式创建数组。一种是使用Array构造函数,给构造函数传入一个数值,然后length属性就会被自动创建并设置为这个值。
let colors = new Array(20) // 创建一个初始length为20的数组
也可以给Array构造函数传入要保存的元素。比如下面的代码会创建一个包含3个字符串值的数组。
let colors = new Array("red", "blue", "green")
有时候想创建一个只含一个数字的数组,直接使用Array构造函数是办不到的,因为他会创建一个长度为传入的数值的数组。这时候可以使用Array.of()方法(ES6新增),这个方法用于将一组参数转换为数组实例。
let colors = new Array(20) // [empty × 20] 创建了一个初始length为20的数组 let nums = Array.of(20) // [20]
另一种创建数组的方式是使用数组字面量(array literal)表示法。
let colors = ["red", "blue", "green"]
Array.from()也可以创建数组,用于将类数组结构转换为数组实例。
Array.from()的第一个参数是一个类数组对象,即任何可迭代的结构,或者有一个length属性和可索引元素的结构。
// 字符串会被拆分成单字符串数组 console.log(Array.from("Ryan")) // ["R", "y", "a", "n"] // Array.from()对现有数组执行浅复制 const a1 = [1, 2, 3, 4] const a2 = Array.from(a1) console.log(a1) // [1, 2, 3, 4] console.log(a1 === a2) // false // arguments对象可以被轻松地转换为数组 function getArgsArray(){ return Array.from(arguments) } console.log(getArgsArray(1, 2, 3)) // [1, 2, 3] // from()也能转换带有必要属性的自定义对象 const arrayLikeObject = { 0: 1, 1: 2, 2: 3, 3: 4, length: 4 } console.log(Array.from(arrayLikeObject)) // [1, 2, 3, 4]
Array.from()还接收第二个可选的映射函数参数。这个函数可以改写新数组的值。还可以接收第三个可选参数,用于指定映射函数中this的值。
const a1 = [1, 2, 3, 4] const a2 = Array.from(a1, x => x 2) const a3 = Array.from(a1, function(x){ return x this.exponent }, { exponent: 2 }) console.log(a2) // [1, 4, 9, 16] console.log(a3) // [1, 4, 9, 16]

6.2.2 数组空位

使用数组字面量初始化数组时,可以使用一串逗号来创建空位(hole)。ECMAScript会将逗号之间相应索引位置的值当成空位,ES6规范重新定义了该如何处理这些空位。
const options = [,,,,,] //创建包含5个元素的数组 console.log(options.length) // 5 console.log(options) // [,,,,,]
for-of循环会将空位当成存在的元素,只不过值为undefined
ES6之前的方法则会忽略这个空位,但具体的行为也会因方法而异。
注意:由于行为不一致和存在性能隐患,实践中要避免使用数组空位。如确实需要空位,则可以显示地用undefined代替

6.2.3 数组索引

要取得或设置数组的值,需要使用中括号并提供相应值的数字索引:
let colors = ["red", "blue", "green"] colors[2] = "black" // 修改第三项
如果把一个值设置给超过数组最大索引的索引,则数组长度会自动扩展到该索引值加1
let colors = ["red", "blue", "green"] colors[5] = "black" // 设置第6项为"black" console.log(colors.length) // 6
通过修改length属性,可以截断或增长数组。增长数组后多出来的空位由undefined填充。
let colors = ["red", "blue", "green"] colors.length = 1 console.log(colors) // ["red"] colors.length = 4 console.log(colors[3]) // undefined

6.2.4 检测数组

instanceof 操作符可以检测一个对象是不是数组。
if(value instanceof Array){ // 操作数组 }
但是如果网页里存在多个框架,可能会涉及两个不同的全局执行上下文,因此会有两个不同版本的Array构造函数,这个时候instanceof不一定返回正确的结果。
为解决这个问题,ECMAScript提供了Array.isArray()方法。这个方法的目的是确定一个值是否为数组,而不用管它在哪个全局执行上下文中创建的。
if(Array.isArray(value)){ // 操作数组 }

6.2.5 迭代器方法

ES6中,Array的原型上暴露了3个用于检索数组内容的方法:keys()、values()和entries()。
keys()返回数组索引的迭代器,values()返回数组元素的迭代器、而entries()返回索引/值对的迭代器。
const a = ["foo", "bar", "baz", "qux"] // 因为这些方法都返回迭代器,所以可以将它们的内容通过Array.from()直接转换为数组实例 const aKeys = Array.from(a.keys()) const aValues = Array.from(a.balues()) const aEntries = Array.from(a.entries()) console.log(aKeys) // [0, 1, 2, 3] console.log(aValues) // ["foo", "bar", "baz", "qux"] console.log(aEntries) // [[0, "foo"], [1, "bar"], [2, "baz"], [3, "qux"]]

6.2.6 复制和填充方法

ES6新增了两个方法:批量赋值方法fill(),以及填充数组方法copyWithin()。
fill()的第一个参数是填充的值,第二个可选参数是索引的开始。第三个可选参数的索引的结束。
const zeroes = [0, 0, 0, 0, 0] zeroes.fill(7, 1, 3) console.log(zeroes) // [0, 7, 7, 0, 0]
copyWithin()会按照指定范围浅复制数组中的部分内容,然后插入到指定索引开始位置。第一个参数是索引开始位置,第二,第三个参数是指定范围的开始索引和结束索引。
const numbers = [1,2,3,4,5,6] numbers.copyWithin(2, 4, 6) console.log(numbers) // [1, 2, 5, 6, 5, 6]

6.2.7 转换方法

所有对象都有toLocaleString()、toString()和valueOf()方法。
其中valueOf()返回的还是数组本身。
toString()返回由数组中每个值调用toString()返回的字符串拼接而成的一个逗号分隔的字符串。
toLocaleString()方法类似toString(),取数组每个元素值的时候会调用toLocaleString()方法,返回字符串拼接而成的一个逗号分隔的字符串。
let colors = ["red", "blue", "green"] console.log(colors.toString()) // "red,blue,green"
调用数组上的join()方法也可以获取字符串,不传参的情况下与toString()的返回值相同,如果传了参数,则分隔符变为参数。
let colors = ["red", "blue", "green"] console.log(colors.join(",")) // "red,green,blue" console.log(colors.join("||")) // "red||green||blue"

6.2.8 栈方法

ECMAScript数组提供了push()和pop()方法,以实现类似栈的行为。
push()方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度。
pop()方法则用于删除数组的最后一项,同时减少数组的length值,返回被删除的项。

6.2.9 队列方法

使用shift()和push()方法,可以把数组当成队列来使用。
shift()方法会删除数组的第一项并返回它。
ECMAScript也提供了unshift()方法,这个方法执行shift()相反的操作:在数组开头添加任意多个值,然后返回新的数组长度。

6.2.10 排序方法

数组有两种方法可以用来对元素重新排序:reverse()和sort()。
reverse()方法将数组元素反向 排列。
let values = [1, 2, 3, 4, 5] values.reverse() console.log(values) // [5, 4, 3, 2, 1]
sort()方法用于排序数组,默认情况下会按照升序排序,最小的值在前面,最大的值在后面。为此sort()会在每一项上调用String()转型函数,然后来比较字符串决定顺序。
let values = [0, 1, 5, 10, 15] values.sort() console.log(values) // [0, 1, 10, 15 ,5] // "5" > "15"
sort()方法接收一个比较函数,用于判断哪个值应该排在前面。
比较函数接收两个参数,如果第一个参数应该排在第二个参数前面,则函数返回负值。如果第一个参数应该排在第二个参数后面,则返回正值。如果参数不用变化位置,则返回0。
function compare(v1, v2){ if(v1 < v2){ return -1 }else if(v1 > v2){ return 1 }else{ return 0 } } /* 等价 function compare(v1, v2){ return v1 - v2 } */ let values = [0, 1, 15, 10, 5] values.sort(compare) console.log(values) // [0, 1, 5, 10, 15]

6.2.11 操作方法

对于数组中的元素,我们有很多操作方法。接下来讲三种方法:concat()、slice()、splice()
concat()方法可以在现有数组全部元素基础上创建一个新的数组。它首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组。如果参数是一个或多个数组,concat()会把数组的每一项添加到结果数组。
let colors = ["red", "green", "blue"] let colors2 = colors.concat("yellow", ["black", "brown"]) console.log(colors2) // ["red", "green", "blue", "yellow", "black", "brown"]
slice()方法用于创建一个包含原始数组中一个或多个元素的新数组。slice()方法可以接收一个或两个参数:返回元素的开始索引和结束索引(不包含),不过不提供第二个参数,则默认从开始索引取到末尾。
let colors = ["red", "green", "blue"] let colors2 = colors.slice(1) let colors3 = colors.slice(1,2) console.log(colors2) // ["green", "blue"] console.log(colors3) // ["green"]
splice()方法可以改变数组本身。它接收三个参数,第一个参数指定删除元素的开始位置,第二个参数指定删除元素的数量,第三个以及之后的参数指定在开始位置处要插入的元素。
let colors = ["red", "green", "blue"] let removed = colors.splice(0, 1) // 在位置0处删除1个元素 console.log(removed) // "red" console.log(colors) // ["green", "blue"] colors.splice(1, 0 ,"a", "b") // 在位置1处删除0个元素,并插入两个元素 console.log(colors) // ["green", "a", "b", "blue"]

6.2.12 搜索和位置方法

ECMAScript提供两类搜索数组的方法:按严格相等搜索和按断言函数搜索。
1. 严格相等
ECMAScript提供了3个严格相等的搜索方法:indexOf()、lastIndexOf()和includes()(ES7)。
这三个方法都接收两个参数:要查找的元素和一个可选的起始搜索位置。
indexOf()和includes()方法从数组前头开始向后搜索,而lastIndexOf()方法相反。
indexOf()和lastIndexOf()返回查找的元素在数组中的位置,如果没找到则返回-1.
includes()返回布尔值,表示是否找到一个指定元素匹配的项。
三个方法在比较时会使用全等(===)比较。
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1] console.log(numbers.indexOf(4)) // 3 console.log(numbers.lastIndexOf(4)) // 5 console.log(numbers.includes(4)) // true console.log(numbers.indexOf(4, 4)) // 5 console.log(numbers.lastIndexOf(4, 4)) // 3 console.log(numbers.includes(4, 7)) // false
2. 断言函数
ECMAScript也允许按照定义的断言函数搜索数组,每个索引都会调用这个函数。
断言函数接收3个参数:元素、索引和数组本身。元素指的是数组中当前搜索的元素。
find()和findIndex()方法使用了断言函数。find()返回第一个匹配的元素,findIndex()返回第一个匹配元素的索引。
let nums = [5, 4, 2, 10] let tenIndex = nums.findIndex(function(v, i, arr){ return v === 10 }) console.log(tenIndex) // 3

6.2.13 迭代方法

ECMAScript为数组定义了5个迭代方法。每个方法接收两个参数:以每一项为参数运行的函数、以及可选的作为函数运行上下文的作用域对象(影响函数中this的值)。传给每个方法的函数接收3个参数:数组元素、元素索引和数组本身。这5个迭代方法如下:
  • every():对数组每一项都运行传入的函数,如果对每一项都返回true,则这个方法返回true。
  • filter():对数组每一项都运行传入的函数,函数返回true的项会组成数组之后返回。
  • forEach():对数组每一项都运行传入的函数,无返回值。
  • map():对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组。
  • some():对数组每一项都运行传入的函数,如果有一项函数返回true,则这个方法返回true,否则返回false
let numbers = [1, 2, 3, 4] let newNums = numbers.map(function(v, i, arr){ return v 2 }) console.log(newNums) // [2, 4, 6, 8]

6.2.14 归并方法

ECMAScript为数组提供了两个归并方法:reduce()和reduceRight()。
这两个方法接收两个参数:第一个参数为初始值,第二个为当前值。每轮循环的返回值都会作为下轮循环的初始值,最后一轮循环的返回值即最终的返回值。
let values = [1, 2, 3, 4] let sum = values.reduce(function(pre, cur){ // 第一次循环,pre为1,cur为2 return pre + cur }) console.log(sum) // 10
如果不提供第二个参数,则默认第一次初始值为第一个元素,当前值则为第二个元素。
如果提供第二个参数,则默认第一次循环的初始值为第二个参数,当前值为第一个元素。
let values = [1, 2, 3, 4] let sum = values.reduce(function(pre, cur){ // 第一次循环,pre为0,cur为1 return pre + cur }, 0) console.log(sum) // 10
reduceRight()和reduce()方法的差别只是方向相反一下。

6.4 Map

作为ECMAScript的新增特性,Map是一种新的集合类型,为这门语言带来了真正的键/值存储机制。Map的大多数特性都可以通过Object类型实现,但二者之间还是存在一些细微的差别。

6.4.1 基本API

使用new关键字和Map构造函数可以创建一个空映射:
const m = new Map()
如果想在创建的同时初始化实例,可以给Map构造函数传入一个可迭代对象,需要包含键/值对数组。
const m1 = new Map([ ["key1", "val1"], ["key2", "val2"], ["key3", "val3"] ]) console.log(m1.size) // 3
可以使用set()方法添加键/值对。可以使用get()和has()进行查询,可以通过size属性获取映射中的键/值对的数量。使用delete()和clear()删除值。
const m = new Map() console.log(m.has("name")) // false console.log(m,size) // 0 m.set("name", "Ryan") console.log(m.has("name")) // true console.log(m.get("name")) // "Ryan" m.delete("name") console.log(m.size) // 0
Map内部使用SameValueZero比较操作,NaN与NaN认为是同一个值,-0 与 +0也认为是一个值。

6.4.2 顺序与迭代

Map的默认迭代器返回entries()(或者Symbol.iterator属性,它引用entries())方法取得的值,也就是以插入顺序生成[key, value]形式的数组。
const m1 = new Map([ ["key1", "val1"], ["key2", "val2"], ["key3", "val3"] ]) for(let pair of m1){ console.log(pair) } // ["key1", "val1"] // ["key2", "val2"] // ["key3", "val3"]
keys()和values()分别返回以插入顺序生成键和值的迭代器。
const m1 = new Map([ ["key1", "val1"], ["key2", "val2"], ["key3", "val3"] ]) for(let key of m1.keys()){ console.log(key) } // "key1" // "key2" // "key3" for(let value of m1.values()){ console.log(value) } // "val1" // "val2" // "val3"

6.4.3 选择object还是Map

如果代码涉及大量删除操作,选择Map,其它情况,Map与object差距不大,一般来说Map内存占用更少。

6.6 Set

ECMAScript6新增的Set是一种新集合类型。Set在很多方面都像是加强的Map,这是因为它们的大多数API和行为都是共有的。

6.6.1 基本API

使用new关键字和Set构造函数创建一个空集合:
const m = new Set()
可以给构造函数传入一个可迭代对象来初始化实例:
const s1 = new Set(["val1", "val2", "val3"]) console.log(s1.size) // 3
初始化之后,可以使用add()增加值,使用has()查询,通过size取得元素数量,以及使用delete()和clear()删除元素。
const s1 = new Set(["val1", "val2", "val3"]) console.log(s1.size) // 3 s1.add("val4") s1.delete("val1") console.log(s1) // ["val2", "val3", "val4"]
Set可以包含任何值,但不允许出现重复的值,集合与Map类似,使用SameValueZero操作(NaN与NaN认为是同一个值,-0 与 +0也认为是一个值)。
const s1 = new Set(["val1", "val2", "val3"]) s1.add("val3") console.log(s1) // ["val1", "val2", "val3"]

6.6.2 顺序与迭代

集合实例可以通过values()方法及其别名方法keys()(或者Symbol.iterator属性,它引用values())取得迭代器(Iterator):
const s1 = new Set(["val1", "val2", "val3"]) console.log(s.values === s[Symbol.iterator]) // true console.log(s.keys === s[Symbol.iterator]) // true for(let value of s.values()){ console.log(value) } // "val1" // "val2" // "val3" for(let value of s[Symbol.iterator]()){ console.log(value) } // "val1" // "val2" // "val3"
集合的entries()方法返回一个迭代器,可以按照插入顺序产生包含两个元素的数组,这两个元素是集合中每个值的重复出现:
for(let pair of s.entries()){ console.log(pair) } // ["val1", "val1"] // ["val2", "val2"] // ["val3", "val3"]

第七章:迭代器与生成器

7.2 迭代器模式

迭代器模式描述了一个方案,即可以把有些结构称为“可迭代对象”,因为它们实现了Iterable接口,而且可以通过Iterable接口生成的迭代器消费。
实现Iterable接口要求具备两种能力
  • 支持迭代的自我识别能力:对象必须暴露Symbol.iterator属性,将这个属性作为“默认迭代器”
  • 创建具有Iterator接口的对象的能力:即默认迭代器(Symbol.iterator)必须返回一个新的迭代器。
数组就是一个可迭代对象
let arr = [1, 2, 3]
它实现了Iterable接口:
let arr = [1, 2, 3] console.log(arr[Symbol.iterator]) // values() { [native code] }
Iterable接口可以生成一个具有Iterator接口的对象(我们叫它迭代器
let arr = [1, 2, 3] console.log(arr[Symbol.iterator]) // values() { [native code] } console.log(arr[Symbol.iterator]()) // Array Iterator {}
接下来讲什么是具有Iterator接口的对象(迭代器)。
迭代器是一种一次性使用的对象,迭代器使用next()方法在可迭代对象中遍历数据。每次成功调用next(),都会返回一个IteratorResult对象,这个对象包含两个属性:done和value,done是一个布尔值,表示是否可以再次调用next(),value包含可迭代对象的下一个值。done为true时,value为undefined,意为“耗尽”。
let arr = ['foo', 'bar'] let iter = arr[Symbol.iterator]() console.log(iter) // Array Iterator{} // 执行迭代 console.log(iter.next()) // { done: false, value: 'foo' } console.log(iter.next()) // { done: false, value: 'foo' } console.log(iter.next()) // { done: true, value: undefined }
为了更方便地使用迭代器,我们可以使用接收可迭代对象的原生语言特性
如 for-of 循环:
let arr = [1, 2, 3, 4] for(let num of arr){ console.log(num) } // 1 // 2 // 3 // 4
其他接收可迭代对象的原生语言特性:
  • for-of 循环
  • 数组解构
  • 扩展操作符
  • Array.from()
  • 创建集合(Set)
  • 创建映射(Map)
  • Promise.all()
  • Promise.race()
  • yield*操作符
我在这里实现一个简单的可迭代对象,帮助大家更好地理解迭代器模式。
// 实现了Iterable接口的对象,并且这个接口返回一个迭代器 let iterable = { [Symbol.iterator](){ let count = 0 // 返回了一个具有Iterator接口的对象(迭代器) return { next(){ /* 每次调用next,count的值会加一,当count等于5的时候停止。 */ if( count ++ < 5){ return { done: false, value: count, } }else{ return { done: true, value: undefined } } } } } } for(let num of iterable){ console.log(num) } // 1 // 2 // 3 // 4 // 5 let arr = [...iterable] console.log(arr) // [1, 2, 3, 4, 5]
迭代器模式,就是可以让本不具备迭代能力的对象,具备可迭代的能力。最直观的体现就是能在for-of循环中直接遍历对象。

7.3 生成器

生成器是ECMAScript6新增的一个极为灵活的结构,拥有一个函数块内暂停和恢复代码执行的能力。可以用作自定义迭代器。

7.3.1 生成器基础

生成器是一个函数,在函数名前面加一个星号(*)表示它是一个生成器:
function * generator(){ }
箭头函数不能用来定义生成器
调用生成器函数会产生一个生成器对象,这个对象也实现了Iterator接口。
生成器对象一开始处于暂停执行(suspended)的状态,调用next()方法,调用这个方法会让生成器开始或恢复执行。
next()方法的返回值类似于迭代器,有一个done属性和一个value属性。函数体为空的生成器函数中间不会停留,调用一次next()就会到达done: true状态。
function * generator(){ } let generatorObj = generator() console.log(generatorObj) // generator {<suspended>} console.log(generatorObj.next()) // {value: undefined, done: true}
value属性是生成器的返回值,默认为undefined,可以通过生成器函数的返回值指定:
function * generator(){ return 'foo' } let generatorObj = generator() console.log(generatorObj) // generator {<suspended>} console.log(generatorObj.next()) // {value: 'foo', done: true}
生成器只会在初次调用next()方法后开始执行:
function * generator(){ console.log(1) } let generatorObj = generator() console.log(generatorObj) // generator {<suspended>} console.log(generatorObj.next()) // 1
生成器对象实现了Iterable接口,它们默认的迭代器是自引用的:
function * generator(){ console.log(1) } let generatorObj = generator() console.log(generatorObj === generatorObj[Symbol.iterator]()) // true

7.3.2 通过yield中断执行

生成器函数在遇到yield关键字之前 会正常执行,但遇到这个关键字后,执行会停止,函数作用域的状态会被保留。必须使用next()方法恢复执行。
yield后跟的值会出现在next()方法返回的对象里(value属性)。
通过yield关键字暂停的生成器函数会处在done: false状态。
function * generator(){ yield 1 yield 2 yield 3 } let generatorObj = generator() console.log(generatorObj.next()) // {value: 1, done: false} console.log(generatorObj.next()) // {value: 2, done: false} console.log(generatorObj.next()) // {value: 3, done: false} console.log(generatorObj.next()) // {value: undefined, done: true}
生成器函数生成的生成器对象不会干扰其他的生成器对象:
function * generator(){ yield 1 yield 2 yield 3 } let generatorObj1 = generator() let generatorObj2 = generator() console.log(generatorObj1.next()) // {value: 1, done: false} console.log(generatorObj2.next()) // {value: 1, done: false}
yield只能在生成器函数内使用,在其他地方会抛出错误。
1. 生成器对象作为可迭代对象
可以将生成器对象作为可迭代对象使用(因为它实现了iterable接口):
function * generator(){ yield 1 yield 2 yield 3 } for(let num of generator()){ console.log(num) } // 1 // 2 // 3
2. 使用yield实现输入和输出
yield 关键字还可以作为函数的中间参数使用。并且在next()函数中传递的参数会成为yield的返回值。
第一次调用next()传入的值不会被使用,因为这次调用是为了开始执行生成器函数:
function * generator(){ console.log('start') let a = yield 1 console.log(a) let b = yield 2 console.log(b) } let generatorObj = generator() console.log(generatorObj.next().value) // 'start' console.log(generatorObj.next('a').value) // 'a' console.log(generatorObj.next('b').value) // 'b'
3. 产生可迭代对象
可以使用星号增强yield的行为,让它能够迭代一个可迭代对象。
function * generator(){ yield* [1, 2, 3] } for(let num of generator()){ console.log(num) } // 1 // 2 // 3
7.3.3 生成器作为默认迭代器
生成器很适合用作实现对象的iterable接口。
改写我们先前的例子:
// 实现了Iterable接口的对象,并且这个接口返回一个迭代器 let iterable = { // 修改为生成器函数 *[Symbol.iterator](){ let count = 0 // 循环,并且使用yield输出值。 while(count ++ < 5){ yield count } } } for(let num of iterable){ console.log(num) } // 1 // 2 // 3 // 4 // 5 let arr = [...iterable] console.log(arr) // [1, 2, 3, 4, 5]
代码变得简洁许多。

第八章:对象、类与面向对象编程

8.1 理解对象

8.1.1 属性的类型

ECMA-262使用一些内部特性来描述属性的特征。为了将某个特性标识为内部特性,规范会用两个中括号把特性的名称括起来,比如[[Enumerable]]。
属性分两种:数据属性和访问器属性。
1. 数据属性
数据属性有4个特性描述它们的行为
  • [[Configurable]]:表示是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。
  • [[Enumerable]]:表示属性是否可以通过for-in循环返回。
  • [[Writable]]:表示属性的值是否可以被修改。
  • [[Value]]:表示属性实际的值。
当属性显示添加到对象后,[[Configurable]],[[Enumerable]],[[Writable]]都会被设置为true,[[Value]]属性会被设置为指定的值,如:
let person = { name: "attackonryan" } console.log(Object.getOwnPropertyDescriptor(person, 'name')) // {value: "attackonryan", writable: true, enumerable: true, configurable: true}
要修改属性的默认特性,就必须使用Object.defineProperty()方法。这个方法接收3个参数:要添加属性的对象、属性的名称和一个描述符对象。
描述符对象上的属性可以包含:configurableenumerablewritablevalue,与特性名一一对应。
let person = {} Object.defineProperty(person, 'name', { writable: false, value: "attackonryan" }) console.log(person.name) // "attackonryan" person.name = "ryan" console.log(person.name) // "attackonryan"
非严格模式下给只读的属性赋值会被忽略,但严格模式下会抛出错误
let person = {} Object.defineProperty(person, 'name', { configurable: false, value: "attackonryan" }) // 抛出错误 Object.defineProperty(person, 'name', { configurable: false, value: "attackonryan" })
调用Object.defineProperty()时,configurableenumerablewritable的值如果不指定,则默认为false。
2. 访问器属性
访问器属性有4个特性描述它们的行为
  • [[Configurable]]:表示是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。
  • [[Enumerable]]:表示属性是否可以通过for-in循环返回。
  • [[Get]]:获取函数,在读取属性时调用。
  • [[Set]]:设置函数,在写入属性时调用。
访问器属性不能直接定义,必须使用Object.defineProperty()。
let person = { _name: "attackonryan" } Object.defineProperty(person, "name", { get(){ return this._name }, set(newVal){ this._name = newVal } }) console.log(person.name) // "attackonryan" person.name = "ryan" console.log(person.name) // "ryan"
获取函数和设置函数不一定都要定义,只定义获取函数意味着属性只读,尝试修改属性会被忽略,严格模式下会抛出错误。同样只有一个设置函数则意味着不能读取,非严格墨水下读取会返回undefined,严格模式下会抛出错误
注意:每个属性只能定义一种属性类型(数据属性 or 访问器属性)

8.1.2 定义多个属性

使用Object.defineProperties()方法可以通过多个描述符一次性定义多个属性。它接收两个参数:要为之添加或修改属性的对象和另一个描述符对象,属性与要修改的属性一一对应。
let person = {} Object.defineProperties(person, { name: { writable: true, value: "attackonryan" }, age: { get(){ return 20 } } })

8.1.3 读取属性的特性

使用Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符。方法接收两个参数:属性所在对象与属性名。
使用Object.getOwnPropertyDescriptors()方法可以取得指定对象的所有属性描述符。

8.1.4 合并对象

Object.assign()方法接收一个目标对象和一个或多个源对象作为函数,将每个源对象中可枚举(Object.propertyIsEnumerable()返回true)且自有(Object.hasOwnProperty()返回true)属性复制到目标对象。 以字符串和符号为键的属性会被复制。
let dest = {} let src = {id: "src"} let result = Object.assign(dest, src) console.log(dest === result) // true console.log(dest !== src) // true console.log(result) // {id: "src"} console.log(dest) // {id: "src"}
Object.assign()执行的是浅复制,如果多个源对象有相同属性,则使用最后一个复制的值。

8.2 创建对象

8.2.1 对象字面量 {}

使用对象字面量 {} 的方式直接进行对象的创建
var person1 = { name: "jack", age: "18", say:function(){} } var person2 = { name: "rose", age: "18", say:function(){} } var person3 = { name: "lili", age: "18", say:function(){} }
存在的问题: 创建多个对象的情况下, 显得不是太方便

8.2.2 new Object()创建对象

使用new Object()
var person1 = new Object() person1.name = "jack" person1.age = "18" person1.say = function(){} var person2 = new Object() person2.name = "rose" person2.age = "18" person2.say = function(){} var person3 = new Object() person3.name = "lili" person3.age = "18" person3.say = function(){}
和对象字面量一样,创建对象显的不是太方便

8.2.3 构造函数创建对象

使用构造函数的形式创建对象, 定义一些重复的特征和行为
function Person(name, age) { this.name = name; this.age = age; this.say = function(){} } var person1 = new Person('小苏苏', '18') var person2 = new Person('小月月', '18') var person3 = new Person('小达儿', '18')
构造函数的执行过程
* 1.Person函数在执行的时候,会自动在函数的内部创建一个Person的实例 * 2.将函数内部的this指向该实例 , 也就是指向Person对象 * 3.函数默认的返回值就是该实例(this所指) * 4.person1接收返回值,故而person1具有name,age...属性 * 在函数开始执行和执行结束的时候,函数内部this都是指向window

8.3 继承

  1. 原型链继承
    1. 实现方式:将子类的原型链指向父类的对象实例
      function Parent(){   this.name = "parent";   this.list = ['a']; } Parent.prototype.sayHi = function(){   console.log('hi'); } function Child(){ } Child.prototype = new Parent(); var child = new Child(); console.log(child.name); child.sayHi();
      原理:子类实例 child 的proto指向 Child 的原型链 prototype,而 Child.prototype 指向 Parent 类的对象实例,该父类对象实例的proto指向 Parent.prototype, 所以 Child 可继承 Parent 的构造函数属性、方法和原型链属性、方法 优点:可继承构造函数的属性,父类构造函数的属性,父类原型的属性 缺点:无法向父类构造函数传参;且所有实例共享父类实例的属性,若父类共有属性为引用类型,一个子类实例更改父类构造函数共有属性时会导致继承的共有属性发生变化;实例如下:
      var a = new Child(); var b = new Child(); a.list.push('b'); console.log(b.list); // ['a','b']
  1. 构造函数继承
    1. 实现方式:在子类构造函数中使用 call 或者 apply 劫持父类构造函数方法,并传入参数
      function Parent(name, id){   this.id = id;   this.name = name;   this.printName = function(){     console.log(this.name);   } } Parent.prototype.sayName = function(){   console.log(this.name); }; function Child(name, id){   Parent.call(this, name, id);   // Parent.apply(this, arguments); } var child = new Child("jin", "1"); child.printName(); // jin child.sayName() // Error
      原理:使用 call 或者 apply 更改子类函数的作用域,使 this 执行父类构造函数,子类因此可以继承父类共有属性 优点:可解决原型链继承的缺点 缺点:不可继承父类的原型链方法,构造函数不可复用
  1. 组合继承
    1. 原理:综合使用构造函数继承和原型链继承
      function Parent(name, id){   this.id = id;   this.name = name;   this.list = ['a'];   this.printName = function(){     console.log(this.name);   } } Parent.prototype.sayName = function(){   console.log(this.name); }; function Child(name, id){   Parent.call(this, name, id);   // Parent.apply(this, arguments); } Child.prototype = new Parent(); var child = new Child("jin", "1"); child.printName(); // jin child.sayName() // jin var a = new Child(); var b = new Child(); a.list.push('b'); console.log(b.list); // ['a']
      优点:可继承父类原型上的属性,且可传参;每个新实例引入的构造函数是私有的 缺点:会执行两次父类的构造函数,消耗较大内存,子类的构造函数会代替原型上的那个父类构造函数
  1. 原型式继承
    1. 原理:类似 Object.create,用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象,结果是将子对象的proto指向父对象
      var parent = {   names: ['a'] } function copy(object) {   function F() {}   F.prototype = object;   return new F(); } var child = copy(parent);
      缺点:共享引用类型
  1. 寄生式继承
    1. 原理:二次封装原型式继承,并拓展
      function createObject(obj) {   var o = copy(obj);   o.getNames = function() {     console.log(this.names);     return this.names;   }   return o; }
      优点:可添加新的属性和方法
  1. 寄生组合式继承
    1. 原理:改进组合继承,利用寄生式继承的思想继承原型
      function inheritPrototype(subClass, superClass) {   // 复制一份父类的原型   var p = copy(superClass.prototype);   // 修正构造函数   p.constructor = subClass;   // 设置子类原型   subClass.prototype = p; } function Parent(name, id){   this.id = id;   this.name = name;   this.list = ['a'];   this.printName = function(){     console.log(this.name);   } } Parent.prototype.sayName = function(){   console.log(this.name); }; function Child(name, id){   Parent.call(this, name, id);   // Parent.apply(this, arguments); } inheritPrototype(Child, Parent);

第十章 函数

10.1. 函数的概念

在 JS 里面,可能会定义非常多的相同代码或者功能相似的代码,这些代码可能需要大量重复使用。虽然 for循环语句也能实现一些简单的重复操作,但是比较具有局限性,此时我们就可以使用 JS 中的函数。
函数:就是封装了一段可被重复调用执行的代码块。通过此代码块可以实现大量代码的重复使用

10.2. 函数的使用

10.2.1. 声明函数

// 声明函数 function 函数名() { //函数体代码 }
  • function 是声明函数的关键字,必须小写
  • 由于函数一般是为了实现某个功能才定义的, 所以通常我们将函数名命名为动词,比如 getSum

10.2.2. 调用函数

// 调用函数 函数名(); // 通过调用函数名来执行函数体代码
  • 调用的时候千万不要忘记添加小括号
  • 口诀:函数不调用,自己不执行
    • 注意:声明函数本身并不会执行代码,只有调用函数时才会执行函数体代码。

10.2.3 函数的封装

  • 函数的封装是把一个或者多个功能通过函数的方式封装起来,对外只提供一个简单的函数接口
  • 简单理解:封装类似于将电脑配件整合组装到机箱中 ( 类似快递打包)
    • 例子:封装计算1-100累加和
      /* 计算1-100之间值的函数 */ // 声明函数 function getSum(){ var sumNum = 0;// 准备一个变量,保存数字和 for (var i = 1; i <= 100; i++) { sumNum += i;// 把每个数值 都累加 到变量中 } alert(sumNum); } // 调用函数 getSum();

10.3 函数的参数

10.3.1 函数参数语法

  • 形参:函数定义时设置接收调用时传入
  • 实参:函数调用时传入小括号内的真实数据
    • 参数的作用 : 在函数内部某些值不能固定,我们可以通过参数在调用函数时传递不同的值进去。
      函数参数的运用:
      // 带参数的函数声明 function 函数名(形参1, 形参2 , 形参3...) { // 可以定义任意多的参数,用逗号分隔 // 函数体 } // 带参数的函数调用 函数名(实参1, 实参2, 实参3...);
      1. 调用的时候实参值是传递给形参的
      1. 形参简单理解为:不用声明的变量
      1. 实参和形参的多个参数之间用逗号(,)分隔

10.3.2 函数形参和实参数量不匹配时

注意:在JavaScript中,形参的默认值是undefined。
小结:
  • 函数可以带参数也可以不带参数
  • 声明函数的时候,函数名括号里面的是形参,形参的默认值为 undefined
  • 调用函数的时候,函数名括号里面的是实参
  • 多个参数中间用逗号分隔
  • 形参的个数可以和实参个数不匹配,但是结果不可预计,我们尽量要匹配

10.4 函数的返回值

10.4.1 return 语句

返回值:函数调用整体代表的数据;函数执行完成后可以通过return语句将指定数据返回 。
// 声明函数 function 函数名(){ ... return 需要返回的值; } // 调用函数 函数名(); // 此时调用函数就可以得到函数体内return 后面的值
  • 在使用 return 语句时,函数会停止执行,并返回指定的值
  • 如果函数没有 return ,返回的值是 undefined

10.4.2 break ,continue ,return 的区别

  • break :结束当前的循环体(如 for、while)
  • continue :跳出本次循环,继续执行下次循环(如 for、while)
  • return :不仅可以退出循环,还能够返回 return 语句中的值,同时还可以结束当前的函数体内的代码

10.5 arguments的使用

当不确定有多少个参数传递的时候,可以用 arguments 来获取。JavaScript 中,arguments实际上它是当前函数的一个内置对象。所有函数都内置了一个 arguments 对象,arguments 对象中存储了传递的所有实参。arguments展示形式是一个伪数组,因此可以进行遍历。伪数组具有以下特点:
  • 具有 length 属性
  • 按索引方式储存数据
  • 不具有数组的 push , pop 等方法
    • 注意:在函数内部使用该对象,用此对象获取函数调用时传的实参。

10.6 函数案例

函数内部可以调用另一个函数,在同一作用域代码中,函数名即代表封装的操作,使用函数名加括号即可以将封装的操作执行。

10.7 函数的两种声明方式

  • 自定义函数方式(命名函数)
    • 利用函数关键字 function 自定义函数方式
      // 声明定义方式 function fn() {...} // 调用 fn();
    • 因为有名字,所以也被称为命名函数
    • 调用函数的代码既可以放到声明函数的前面,也可以放在声明函数的后面
  • 函数表达式方式(匿名函数)
    • 利用函数表达式方式的写法如下:
      // 这是函数表达式写法,匿名函数后面跟分号结束 var fn = function(){...}; // 调用的方式,函数调用必须写到函数体下面 fn();
    • 因为函数没有名字,所以也被称为匿名函数
    • 这个fn 里面存储的是一个函数
    • 函数表达式方式原理跟声明变量方式是一致的
    • 函数调用的代码必须写到函数体后面

第十二章 BOM

BOM(Browser Object Model)即浏览器对象模型,它提供了独立于内容而与浏览器窗口进行交互的对象,其核心对象是 window。
BOM 由一系列相关的对象构成,并且每个对象都提供了很多方法与属性。
BOM 缺乏标准,JavaScript 语法的标准化组织是 ECMA,DOM 的标准化组织是 W3C,BOM 最初是Netscape 浏览器标准的一部分。
notion image

12.4.1 页面(窗口)加载事件(2种)

第1种
window.onload 是窗口 (页面)加载事件,当文档内容完全加载完成会触发该事件(包括图像、脚本文件、CSS 文件等), 就调用的处理函数。
第2种
DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片,flash等等。
IE9以上才支持!!!
如果页面的图片很多的话, 从用户访问到onload触发可能需要较长的时间, 交互效果就不能实现,必然影响用户的体验,此时用 DOMContentLoaded 事件比较合适。
<script> window.addEventListener('load', function() { var btn = document.querySelector('button'); btn.addEventListener('click', function() { alert('点击我'); }) }) window.addEventListener('load', function() { alert(22); }) document.addEventListener('DOMContentLoaded', function() { alert(33); }) </script>

12.4.2 调整窗口大小事件

window.onresize 是调整窗口大小加载事件, 当触发时就调用的处理函数。
注意:
  1. 只要窗口大小发生像素变化,就会触发这个事件。
  1. 我们经常利用这个事件完成响应式布局。 window.innerWidth 当前屏幕的宽度
<script> // 注册页面加载事件 window.addEventListener('load', function() { var div = document.querySelector('div'); // 注册调整窗口大小事件 window.addEventListener('resize', function() { // window.innerWidth 获取窗口大小 console.log('变化了'); if (window.innerWidth <= 800) { div.style.display = 'none'; } else { div.style.display = 'block'; } }) }) </script> <div></div>

12.5 定时器(两种)

window 对象给我们提供了 2 个非常好用的方法-定时器。
  • setTimeout()
  • setInterval()

12.5.1 setTimeout() 定时器

12.5.1.1 开启定时器

普通函数是按照代码顺序直接调用。 简单理解: 回调,就是回头调用的意思。上一件事干完,再回头再调用这个函数。 例如:定时器中的调用函数,事件处理函数,也是回调函数。 以前我们讲的 element.onclick = function(){} 或者 element.addEventListener(“click”, fn); 里面的 函数也是回调函数。
<script> // 回调函数是一个匿名函数 setTimeout(function() { console.log('时间到了'); }, 2000); function callback() { console.log('爆炸了'); } // 回调函数是一个有名函数 var timer1 = setTimeout(callback, 3000); var timer2 = setTimeout(callback, 5000); </script>

12.5.1.2 停止定时器

<button>点击停止定时器</button> <script> var btn = document.querySelector('button'); // 开启定时器 var timer = setTimeout(function() { console.log('爆炸了'); }, 5000); // 给按钮注册单击事件 btn.addEventListener('click', function() { // 停止定时器 clearTimeout(timer); }) </script>

12.5.2 setInterval() 定时器

12.5.2.1 开启定时器

<script> // 1. setInterval setInterval(function() { console.log('继续输出'); }, 1000); </script>

12.6 location对象

21.6.4 location对象的常见方法

<button>点击</button> <script> var btn = document.querySelector('button'); btn.addEventListener('click', function() { // 记录浏览历史,所以可以实现后退功能 // location.assign('http://www.itcast.cn'); // 不记录浏览历史,所以不可以实现后退功能 // location.replace('http://www.itcast.cn'); location.reload(true); }) </script>

12.7 navigator对象

navigator 对象包含有关浏览器的信息,它有很多属性,我们最常用的是 userAgent,该属性可以返回由客户机发送服务器的 user-agent 头部的值。
下面前端代码可以判断用户那个终端打开页面,实现跳转
if((navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i))) { window.location.href = ""; //手机 } else { window.location.href = ""; //电脑 }

12.8 history对象

window对象给我们提供了一个 history对象,与浏览器历史记录进行交互。该对象包含用户(在浏览器窗口中)访问过的URL。

第十四章 DOM

文档对象模型(Document Object Model,简称DOM),是 W3C 组织推荐的处理可扩展标记语言(html或者xhtml)的标准编程接口
W3C 已经定义了一系列的 DOM 接口,通过这些 DOM 接口可以改变网页的内容、结构和样式。
DOM是W3C组织制定的一套处理 html和xml文档的规范,所有的浏览器都遵循了这套标准。
DOM树 又称为文档树模型,把文档映射成树形结构,通过节点对象对其处理,处理的结果可以加入到当前的页面。
  • 文档:一个页面就是一个文档,DOM中使用document表示
  • 节点:网页中的所有内容,在文档树中都是节点(标签、属性、文本、注释等),使用node表示
  • 标签节点:网页中的所有标签,通常称为元素节点,又简称为“元素”,使用element表示

第十七章 事件

17.1 注册事件(2种方式)
notion image

17.2 事件监听

addEventListener()

eventTarget.addEventListener()方法将指定的监听器注册到 eventTarget(目标对象)上,当该对象触发指定的事件时,就会执行事件处理函数。

attacheEvent()

eventTarget.attachEvent()方法将指定的监听器注册到 eventTarget(目标对象) 上,当该对象触发指定的事件时,指定的回调函数就会被执行。

17.4 DOM事件流

html中的标签都是相互嵌套的,我们可以将元素想象成一个盒子装一个盒子,document是最外面的大盒子。 当你单击一个div时,同时你也单击了div的父元素,甚至整个页面。 那么是先执行父元素的单击事件,还是先执行div的单击事件 ???
比如:我们给页面中的一个div注册了单击事件,当你单击了div时,也就单击了body,单击了html,单击了document。
DOM 事件流会经历3个阶段:
  1. 捕获阶段
  1. 当前目标阶段
  1. 冒泡阶段
我们向水里面扔一块石头,首先它会有一个下降的过程,这个过程就可以理解为从最顶层向事件发生的最具体元素(目标点)的捕获过程;之后会产生泡泡,会在最低点( 最具体元素)之后漂浮到水面上,这个过程相当于事件冒泡。
事件冒泡
<div class="father"> <div class="son">son盒子</div> </div> <script> // onclick 和 attachEvent(ie) 在冒泡阶段触发 // 冒泡阶段 如果addEventListener 第三个参数是 false 或者 省略 // son -> father ->body -> html -> document var son = document.querySelector('.son'); // 给son注册单击事件 son.addEventListener('click', function() { alert('son'); }, false); // 给father注册单击事件 var father = document.querySelector('.father'); father.addEventListener('click', function() { alert('father'); }, false); // 给document注册单击事件,省略第3个参数 document.addEventListener('click', function() { alert('document'); }) </script>
事件捕获
<div class="father"> <div class="son">son盒子</div> </div> <script> // 如果addEventListener() 第三个参数是 true 那么在捕获阶段触发 // document -> html -> body -> father -> son var son = document.querySelector('.son'); // 给son注册单击事件,第3个参数为true son.addEventListener('click', function() { alert('son'); }, true); var father = document.querySelector('.father'); // 给father注册单击事件,第3个参数为true father.addEventListener('click', function() { alert('father'); }, true); // 给document注册单击事件,第3个参数为true document.addEventListener('click', function() { alert('document'); }, true) </script>

17.5 事件对象

17.5.1 什么是事件对象

事件发生后,跟事件相关的一系列信息数据的集合都放到这个对象里面,这个对象就是事件对象。
比如:
  1. 谁绑定了这个事件。
  1. 鼠标触发事件的话,会得到鼠标的相关信息,如鼠标位置。
  1. 键盘触发事件的话,会得到键盘的相关信息,如按了哪个键。

17.5.2 事件对象的使用

事件触发发生时就会产生事件对象,并且系统会以实参的形式传给事件处理函数。
所以,在事件处理函数中声明1个形参用来接收事件对象。

17.5.3 事件对象的兼容性处理

事件对象本身的获取存在兼容问题:
  1. 标准浏览器中是浏览器给方法传递的参数,只需要定义形参 e 就可以获取到。
  1. 在 IE6~8 中,浏览器不会给方法传递参数,如果需要的话,需要到 window.event 中获取查找。
只要“||”前面为false, 不管“||”后面是true 还是 false,都返回 “||” 后面的值。 只要“||”前面为true, 不管“||”后面是true 还是 false,都返回 “||” 前面的值。
<div>123</div> <script> var div = document.querySelector('div'); div.onclick = function(e) { // 事件对象 e = e || window.event; console.log(e); } </script>

17.5.4 事件对象的属性和方法

17.5.5 e.target 和 this 的区别

  • this 是事件绑定的元素(绑定这个事件处理函数的元素) 。
  • e.target 是事件触发的元素。
常情况下terget 和 this是一致的, 但有一种情况不同,那就是在事件冒泡时(父子元素有相同事件,单击子元素,父元素的事件处理函数也会被触发执行), 这时候this指向的是父元素,因为它是绑定事件的元素对象, 而target指向的是子元素,因为他是触发事件的那个具体元素对象。
<div>123</div> <script> var div = document.querySelector('div'); div.addEventListener('click', function(e) { // e.target 和 this指向的都是div console.log(e.target); console.log(this); }); </script>
事件冒泡下的e.target和this
<ul> <li>abc</li> <li>abc</li> <li>abc</li> </ul> <script> var ul = document.querySelector('ul'); ul.addEventListener('click', function(e) { // 我们给ul 绑定了事件 那么this 就指向ul console.log(this); // ul // e.target 触发了事件的对象 我们点击的是li e.target 指向的就是li console.log(e.target); // li }); </script>

18.5.6 阻止默认行为

html中一些标签有默认行为,例如a标签被单击后,默认会进行页面跳转。
<a href="http://www.baidu.com">百度</a> <script> // 2. 阻止默认行为 让链接不跳转 var a = document.querySelector('a'); a.addEventListener('click', function(e) { e.preventDefault(); // dom 标准写法 }); // 3. 传统的注册方式 a.onclick = function(e) { // 普通浏览器 e.preventDefault(); 方法 e.preventDefault(); // 低版本浏览器 ie678 returnValue 属性 e.returnValue = false; // 我们可以利用return false 也能阻止默认行为 没有兼容性问题 return false; } </script>

17.5.7 阻止事件冒泡

事件冒泡本身的特性,会带来的坏处,也会带来的好处。
<div class="father"> <div class="son">son儿子</div> </div> <script> var son = document.querySelector('.son'); // 给son注册单击事件 son.addEventListener('click', function(e) { alert('son'); e.stopPropagation(); // stop 停止 Propagation 传播 window.event.cancelBubble = true; // 非标准 cancel 取消 bubble 泡泡 }, false); var father = document.querySelector('.father'); // 给father注册单击事件 father.addEventListener('click', function() { alert('father'); }, false); // 给document注册单击事件 document.addEventListener('click', function() { alert('document'); }) </script>
阻止事件冒泡的兼容性处理

17.5.8 事件委托

事件冒泡本身的特性,会带来的坏处,也会带来的好处。

17.5.8.1 什么是事件委托

把事情委托给别人,代为处理。
事件委托也称为事件代理,在 jQuery 里面称为事件委派。
说白了就是,不给子元素注册事件,给父元素注册事件,把处理代码在父元素的事件中执行。
生活中的代理:
js事件中的代理:

17.5.8.2 事件委托的原理

给父元素注册事件,利用事件冒泡,当子元素的事件触发,会冒泡到父元素,然后去控制相应的子元素。

17.5.8.3 事件委托的作用

  • 我们只操作了一次 DOM ,提高了程序的性能。
  • 动态新创建的子元素,也拥有事件。
<ul> <li>知否知否,点我应有弹框在手!</li> <li>知否知否,点我应有弹框在手!</li> <li>知否知否,点我应有弹框在手!</li> <li>知否知否,点我应有弹框在手!</li> <li>知否知否,点我应有弹框在手!</li> </ul> <script> // 事件委托的核心原理:给父节点添加侦听器, 利用事件冒泡影响每一个子节点 var ul = document.querySelector('ul'); ul.addEventListener('click', function(e) { // e.target 这个可以得到我们点击的对象 e.target.style.backgroundColor = 'pink'; }) </script>
 

《算法导论》读书笔记 《算法导论》读书笔记