五、JS数组
1. 什么是数组
数组(Array),顾名思义:用来存储一组相关值的类型。
数组可以方便地对一组值进行求和、计算平均值、逐项遍历等操作。
java : int[] array = new int[]{1,2,3} int[] array = {1,2,3} int[] array = new int[3] var scoreArr = [87, 89, 93, 71, 100, 68, 94, 88]; //按索引查找o(1) arr[索引] //按值查找 循环遍历比较o(n) //链表 数据域data,指针域next 50% //增删
数组名习惯以 Arr 结尾。
2. 数组的定义
2.1 方括号定义法
var arr = ['A', 'B', 'C', 'D'];
2.2 new 定义法
var arr = new Array('A', 'B', 'C', 'D');
var arr = new Array(4); // 定义一个长度为 4 的数组,但是这 4 项都是 undefined
两种定义方法根据实际需求选择即可,两者的底层都是同样的实现逻辑。
推荐:方括号定义法!
如果是定义时就要指定数组的值,那么建议使用:
var arr = ['A', 'B', 'C', 'D'];
如果是定义时还不指定数组的值,那么建议使用:
var arr = [];
3. 访问数组项
注意:JS 中数组的元素可以是不同的数据类型!
数组每一项都有下标,下标从 0 开始!
可以使用 数组名[下标]
的形式,访问数组的任一项。
4. 下标越界
JS 规定,访问数组中不存在的项会返回 undefined
,不会报错!
5. 数组的长度
数组的 length
属性表示它的长度。
数组是引用类型,有自己的属性和方法。
var arr = ['A', 'B', 'C', 'D']; console.log(arr.length); // 4
数组最后一项的下标是数组的长度减 1。
6. 更改数组项
-
访问数组项
-
更改数组值
如果更改的数组项超过了 length-1
,则会创造该项。
JS 数组是可以动态扩容的!(和其他语言的区别)
var arr = [1, 2, 3, 4]; arr[6] = 0; console.log(arr); // [1, 2, 3, 4, undefined, undefined, 0]
7. 数组的遍历
数组最大的优点就是方便遍历。
var arr = [1, 2, 3, 4]; for (var i = 0; i < arr.length; i++) { console.log(arr[i]); } var a = ['A', 'B', 'C']; for (var v of a) { console.log(v); // 'A', 'B', 'C' }
8. 数组类型的检测
数组用 typeof
检测结果是 object
。
Array.isArray()
方法可以用来检测数组,返回一个布尔值。
Array.isArray([1, 2, 3]); // true Array.isArray([]); // true
9. 数组的常用方法
9.1 数组的头尾操作
方法 | 功能 |
---|---|
push() | 在尾部插入新项 |
pop() | 在尾部删除 |
unshift() | 在头部插入新项 |
shift() | 在头部删除 |
9.1.1 push() 方法
push()
方法用来在数组末尾推入新项,参数就是要推入的项。
如果要推入多项,可以用逗号隔开。
调用 push()
方法后,数组会立即改变,不需要赋值。
var arr = [22, 33, 44, 55]; arr.push(66); arr.push(77, 88, 99); console.log(arr); // [22, 33, 44, 55, 66, 77, 88, 99]
9.1.2 pop() 方法
与 push() 方法相反,pop()
方法用来删除数组中的最后一项。
()
里没有参数,默认弹出最后一项。
pop()
默认返回最后一项的值。
var arr = [22, 33, 44, 55]; var item = arr.pop(); console.log(arr); // [22, 33, 44] console.log(item); // 55
9.1.3 unshift() 方法
unshift()
方法用来在数组头部插入新项,参数就是要插入的项。
如果要插入多项,可以用逗号隔开。
调用 unshift()
方法后,数组会立即改变,不需要赋值。
9.1.4 shift() 方法
与 unshift() 方法相反,shift()
方法用来删除数组中的开头一项。
()
里没有参数,默认弹出开头一项。
shift()
默认返回开头一项的值。
9.2 splice() 方法
splice()
方法用于替换数组中的指定项。
由于 splice() 可以实现很多功能,所以也称为 JS 的 “多功能方法”。
-
替换项
var arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; // 从下标为 3 的项开始,连续替换 2 项。即将 'D', 'E' 替换为 'X', 'Y', 'Z' arr.splice(3, 2, 'X', 'Y', 'Z'); console.log(arr); // ['A', 'B', 'C', 'X', 'Y', 'Z', 'F', 'G']
-
插入项
var arr = ['A', 'B', 'C', 'D']; // 从下标为 2 的项开始,连续替换 0 项,即:在 [2] 处插入。 arr.splice(2, 0, 'X', 'Y', 'Z'); console.log(arr); // ['A', 'B', 'X', 'Y', 'Z', 'C', 'D']
-
删除项
var arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; // 从下标为 2 的项开始,连续替换 4 项(替换为空,即:删除)。 arr.splice(2, 4); console.log(arr); // ['A', 'B', 'G']
splice()
方法会以数组形式返回被替换/删除的项。
var arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; // 从下标为 3 的项开始,连续替换 2 项。 var item = arr.splice(3, 2, 'X', 'Y', 'Z'); console.log(arr); // ['A', 'B', 'C', 'X', 'Y', 'Z', 'F', 'G'] console.log(item); // ['D', 'E'] var arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; // 从下标为 2 的项开始,连续替换 4 项(替换为空,即:删除)。 var item = arr.splice(2, 4); console.log(arr); // ['A', 'B', 'G'] console.log(item); // ['C', 'D', 'E', 'F']
9.3 slice() 方法
slice()
方法用于得到子数组,类似于字符串中的 slice() 方法。
slice(a, b)
截取的子数组从下标为 a 的项开始,到下标为 b(但不包括下标为 b 的项)结束。
slice(a, b)
方法不会更改原有的数组。
slice()
如果不提供第二个参数,则表示从指定项开始,提取后续所有项作为子数组。
slice()
方法的参数允许为负数,表示数组的倒数第几项(记住不包括最后一项)。
var arr = ['A', 'B', 'C', 'D', 'E', 'F']; var childArr1 = arr.slice(2, 5); var childArr2 = arr.slice(2); var childArr3 = arr.slice(2, -1); console.log(arr); // ['A', 'B', 'C', 'D', 'E', 'F'] console.log(childArr1); // ['C', 'D', 'E'] console.log(childArr2); // ['C', 'D', 'E', 'F'] console.log(childArr3); // ['C', 'D', 'E']
9.4 join() 和 split() 方法
数组的 join()
方法可以使 数组 转为 字符串。
字符串的 split()
方法可以使 字符串 转为 数组。
-
join()
的参数表示以什么字符作为连接符,如果留空则默认以逗号分隔,如同调用toString()
方法。 -
split()
的参数表示以什么字符拆分字符串,一般不能留空。
[22, 33, 44, 55].join(); // "22,33,44,55" [22, 33, 44, 55].toString(); // "22,33,44,55" [22, 33, 44, 55].join(','); // "22,33,44,55" [22, 33, 44, 55].join('-'); // "22-33-44-55" [22, 33, 44, 55].join('~'); // "22~33~44~55"'abcdefg'.split(); // ["abcdefg"] 'abcdefg'.split(''); // ["a", "b", "c", "d", "e", "f", "g"] 'a-b-c-d-e-f-g'.split(''); // ["a", "-", "b", "-", "c", "-", "d", "-", "e", "-", "f", "-", "g"] 'a-b-c-d-e-f-g'.split('-'); // ["a", "b", "c", "d", "e", "f", "g"]
9.5 字符串和数组更多相关性
字符串也可以使用 [下标]
的形式访问某个字符,等价于 charAt()
方法。
在对字符串中的字符进行遍历时不用转为数组,直接利用 [下标] 即可!
'我爱前端'[0]; // "我" '我爱前端'[1]; // "爱" '我爱前端'.charAt(0); // "我"var str = '我爱前端'; for (var i = 0; i < str.length; i++) { console.log(str[i]); }
9.6 concat() 方法
concat()
方法可以合并连接多个数组(以返回值的形式)。
concat()
方法不会改变原数组。
var arr1 = [1, 2, 3, 4]; var arr2 = [5, 6, 7, 8]; var arr3 = [9, 10, 11]; var arr = arr1.concat(arr2, arr3); console.log(arr); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
9.7 reverse() 方法
reverse()
方法用来将一个数组中的全部项顺序置反。
var arr = ['A', 'B', 'C', 'D']; arr.reverse(); console.log(arr); // ["D", "C", "B", "A"]
【小案例】
字符串 'ABCDEFG'
逆序。
'ABCDEFG'.split('').reverse().join(''); // "GFEDCBA"
9.8 indexOf() 和 includes() 方法
indexOf()
方法的功能是搜索数组中的元素,并返回它所在的位置,如果元素不存在,则返回 -1。
includes()
方法的功能是判断一个数组是否包含一个指定的值,返回一个布尔值。
['A', 'B', 'C', 'D'].indexOf('C'); // 2 ['A', 'B', 'C', 'D'].indexOf('D'); // 3 ['A', 'B', 'C', 'D'].indexOf('X'); // -1 ['A', 'B', 'B', 'B'].indexOf('B'); // 1['A', 'B', 'C', 'D'].includes('D'); // true ['A', 'B', 'C', 'D'].includes('X'); // false
注意:indexOf()
及 includes()
方法的判断标准为 ===
全相等!
[11, 22, 33].includes('22'); // false [11, 22, 33].indexOf('22'); // -1
9.9 sort() 方法
sort()
方法用原地算法(直接改变原数组)对数组的元素进行排序,并返回数组。(默认升序,从小到大)
默认排序顺序是将元素转换为字符串,然后根据字典序进行排序(数字 ——> 大写字母 ——> 小写字母,字符串内逐个字符进行比较,相同时比较下一位)
var arr = [3, 18, 10, 24]; console.log(arr.sort()); // [ 10, 18, 24, 3 ] var arr = ['A', 'a', 'c', 'D', 1]; console.log(arr.sort()); // [ 1, 'A', 'D', 'a', 'c' ] var arr = ['aa', 'a0', 'aA', 'A1', 'Aa', 'AA']; console.log(arr.sort()); // [ 'A1', 'AA', 'Aa', 'a0', 'aA', 'aa' ]
sort()
方法可以接收一个函数作为参数,我们可以在这个函数中自定义我们的排序规则:
arr.sort(function(a, b) { // 比较规则... }); // 参数a:前一个用于比较的元素。 // 参数b:后一个用于比较的元素。
function(a, b) 返回值 | 排序顺序 |
---|---|
> 0 | a 在 b 后 |
< 0 | a 在 b 前 |
=== 0 | 保持 a 和 b 的顺序 |
案例1:按数字大小进行排序(降序):
var arr = [3, 18, 10, 24]; arr.sort(function(a, b) { if (a > b) { return -1; } else if (a < b) { return 1; } else { return 0; } }); console.log(arr); // [ 24, 18, 10, 3 ]
案例2:按照学生分数进行排序(降序):
var students = [ { name: 'Edward', score: 66 }, { name: 'Sharpe', score: 84 }, { name: 'And', score: 58 }, { name: 'The', score: 92 }, { name: 'Magnetic', score: 99 }, { name: 'Zeros', score: 74 } ]; students.sort(function (a, b) { if (a.score > b.score) { return -1; } else if (a.score < b.score) { return 1; } else { return 0; } }); console.log(students); /* [ { name: 'Magnetic', score: 99 }, { name: 'The', score: 92 }, { name: 'Sharpe', score: 84 }, { name: 'Zeros', score: 74 }, { name: 'Edward', score: 66 }, { name: 'And', score: 58 } ] */
除了内置排序方法外,还有一些排序算法:冒泡排序
和 快速排序
将在后面介绍。
9.10 数组去重和随机样本(案例)
【数组去重】
题目:去掉数组中的重复项。
思路:准备一个空结果数组,遍历原数组,如果遍历到的项不在结果数组中,则推入结果数组。
var arr = [1, 1, 1, 2, 2, 3, 3, 3, 2, 1]; var resultArr = []; for (var i = 0; i < arr.length; i++) { if (!resultArr.includes(arr[i])) { resultArr.push(arr[i]); } } console.log(resultArr);
【随机样本】
题目:请随机从原数组中取 3 项。
思路:准备一个空结果数组,遍历原数组,随机选择一项,推入结果数组,并且将这项在原数组中删除。
var arr = [3, 6, 10, 5, 8, 9]; var resultArr = []; for (var i = 0; i < 3; i++) { var n = parseInt(Math.random() * arr.length); resultArr.push(arr[n]); arr.splice(n, 1); } console.log(resultArr);
9.11 冒泡排序(算法)*(课后练习)
冒泡排序是一个著名的排序算法,也是最基础的交换排序。
冒泡排序的核心思想:一趟一趟地进行多次项的两两比较,每次都会将最大的元素排好位置,如同水中的气泡上浮一样。
时间复杂度:
O(n²)
var arr = [9, 5, 6, 8, 2, 7, 3, 4, 1]; var temp; for (var i = 0; i < arr.length - 1; i++) { for (var j = i + 1; j < arr.length; j++) { if (arr[i] > arr[j]) { temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } } console.log(arr);##
10. 二维数组*(选学)
二维数组:以数组作为数组元素的数组,即 “数组的数组”。
二维数组可以看做是 “矩阵”。
matrix:矩阵
var matrix = [ [11, 33, 55], [22, 33, 44], [36, 49, 52], [56, 10, 23] ]; // 二维数组长度 console.log(matrix.length); // 4 console.log(matrix[1].length); // 3 // 遍历二维数组 for (var i = 0; i < matrix.length; i++) { for (var j = 0; j < matrix[i].length; j++) { console.log(matrix[i][j]); } }
11. 基本类型值和引用类型值*(选学)
【当 var a = b
赋值时】
-
基本类型值:内存中产生新的副本
-
引用类型值:内存中不产生新的副本,而是让新变量指向同一个对象(内存地址的副本)
// 基本类型值 var a = 3; var b = a; a++; console.log(a); // 4 consloe.log(b); // 3// 引用类型值 var arr1 = [1, 2, 3, 4]; var arr2 = arr1; arr1.push(5); console.log(arr1); // [1, 2, 3, 4, 5] console.log(arr2); // [1, 2, 3, 4, 5]
-
基本类型:
number
、boolean
、string
、undefined
、null
-
引用类型:
array
、object
、function
、regexp
、……
【相等 ==
判断时的区别】
-
基本类型进行相等
==
判断时,会比较 “值” 是否相等 -
引用类型进行相等
==
判断时,会比较 “址” 是否相等,也就是说它会比较是否为内存中的同一个对象
基本类型:
==
(自动转型后,比较值);===
(类型相同的情况下,比较值)引用类型:
==
和===
效果一样!(因为当内存地址相同时,类型肯定相同)
3 == 3; // true 3 === 3; // true 3 == '3'; // true 3 === '3'; // false [1, 2, 3] == [1, 2, 3]; // false [1, 2, 3] === [1, 2, 3]; // false [] == []; // false [] === []; // false var arr = [1, 2, 3]; arr == arr; // true var arr2 = arr; arr == arr2; // true,这里比较的是 arr 与 arr2 中保存的地址是否相等 arr === arr2; // true,由于类型相同,所以这里比较的也是 arr 与 arr2 中保存的地址是否相等
12. 深克隆和浅克隆*(选学)
深克隆和浅克隆需要手写代码实现,而不是简单的调用函数。
使用 arr1 = arr2 的语法不会克隆数组(两个变量指向同一个内存地址)。
浅克隆:只克隆数组的第一层,如果是多维数组,或者数组中的项是其他引用类型值,则不克隆其他更深层。
深克隆:克隆数组的所有层,要用递归方法,在之后介绍。
【浅克隆】
核心思想:“藕断丝连”
var arr = [1, 2, 3, 4, [5, 6, 7]]; var resultArr = []; for (var i = 0; i < arr.length; i++) { resultArr.push(arr[i]); } console.log(resultArr); // [1, 2, 3, 4, [5, 6, 7]] console.log(resultArr == arr); // false console.log(resultArr[4] == arr[4]); // true
六、JS函数
1. 什么是函数(方法)
函数就是语句的封装,可以让这些代码方便地被复用。
函数具有 “一次定义,多次调用” 的优点。
使用函数,可以简化代码,让代码更具有可读性。
2. 函数的定义
和变量类似,函数必须先定义然后才能使用。
使用 function
关键字定义函数。
function:函数、功能。
【方式 1 函数声明】
function fun() { // 函数语句块 }
-
function
:定义函数的关键字 -
fun
:函数名(必须符合 JS 标识符命名规则) -
()
:圆括号中是形参列表,即使没有形参,也必须书写圆括号 -
{}
:花括号内为函数语句块
【方式 2 函数表达式】
var fun = function() { // 函数语句块 };
-
function(){}
:匿名函数 -
()
:圆括号中是形参列表,即使没有形参,也必须书写圆括号 -
{}
:花括号内为函数语句块 -
fun
:变量(指向一个函数)
风格说明:
function fun() { } // 末尾不需要加 ;var fun = function() { }; // 末尾应该加上 ;(因为这是一个赋值语句)
函数声明可以被提升,函数表达式不会被提升
函数声明: 函数声明会被提升到它们所在作用域的顶部,这意味着你可以在函数声明之前调用它。
javascriptCopy CodemyFunction(); // 输出 "This is a function declaration." function myFunction() { console.log("This is a function declaration."); }
函数表达式: 函数表达式不会被提升。变量声明会被提升,但赋值(即函数体)不会被提升,这意味着如果你在赋值之前调用这个函数,会得到一个TypeError
。
javascriptCopy CodemyFunction(); // TypeError: myFunction is not a function const myFunction = function() { console.log("This is a function expression."); };
3. 函数的调用
执行函数体中的所有语句,就称为 “调用函数”。
调用函数非常简单,只需要在函数名字后书写圆括号对即可。
fun(); // 调用函数
【小案例】
// 定义函数,定义的函数是不会被立即执行的 function fun() { console.log('你好'); console.log('今天天气真好'); } // 函数必须要等到调用的时候才能被执行 fun(); fun(); fun(); // 执行了三次
4. 函数声明的提升
和变量声明提升类似,函数声明也可以被提升。
fun(); // 在预解析阶段会被提升 function fun() { alert("函数被执行"); }
效果相当于:
function fun() { alert("函数被执行"); } fun();
【函数表达式不能被提升】
fun(); // 报错! var fun = function() { alert("函数不能被执行"); };
解释:函数表达式不能被提升的本质原因是函数表达式定义的其实是个变量,只不过是把函数赋给这个变量,而变量的提升只提升定义,不提升赋值!
还是那个原则:先定义再使用!
5. 函数优先提升
可以简单理解为:函数提升程度 > 变量提升程度。
fun(); // B var fun = function () { alert('A'); }; function fun() { alert('B'); } fun(); // A
效果相当于:
function fun() { alert('B'); } var fun; fun(); // B fun = function () { alert('A'); }; fun(); // A
6. 函数的参数和返回值
6.1 函数的参数
参数是函数内的一些待定值,在调用函数时,必须传入这些参数的具体值。
函数的参数可多可少,函数可以没有参数,也可以有多个参数,多个参数之间需要用逗号隔开。
// 形参 function add(a, b) { var sum = a + b; console.log('a + b = ' + sum); } // 实参 add(3, 5);
-
圆括号中定义 “形式参数”
-
调用函数时传入 “实际参数”
“形式参数” 和 “实际参数” 是彼此独立的,除了传递值(复制值)之外,互不干扰!
注意:JS 只有 “值传递” 没有 “引用传递”,对于引用类型的传递,传递的不是引用,而是那个变量里面的值(引用的地址)。
引用传递:修改形参,实参也会改变。JS 中复杂类型的实参是个地址值不需要改变,也改变不了,改变的是地址所指向的堆中的复杂类型的具体值,此处具有迷惑性,要加以辨别!
6.1.1 “形实结合”
6.1.2 形参和实参个数不同的情况
6.1.3 arguments
函数内 arguments
表示它接收到的实参列表,它是一个类数组对象。
类数组对象:所有属性均为从 0
开始的自然数序列,并且有 length
属性,和数组类似可以用方括号书写下标访问对象的某个属性值,但是不能调用数组的方法。
function fun() { console.log(arguments); // 11 22 33 44 console.log(arguments[0]); // 11 console.log(arguments[1]); // 22 console.log(arguments[9]); // undefined } fun(11, 22, 33, 44);
【小案例】
JS 本身没有函数的重载(函数名相同,形参个数不同),但是可以借助 arguments 模拟 “函数重载”。
以下例子是一个典型的 “函数重载”,参数个数不同形成 “重载”。
function fun() { if (arguments.length == 0) { console.log(0); } else if (arguments.length == 1) { console.log(1); } else { console.log(2); } } fun(); // 0 fun(1); // 1 fun(1, 2); // 2
6.2 函数的返回值
函数体内可以使用 return
关键字表示 “函数的返回值”。
function sum(a, b) { return a + b; // 函数的返回值 } var result = sum(3, 5); // 函数的返回值可以被变量接收
调用一个有返回值的函数,可以被当做一个普通值,从而可以出现在任何可以书写值的地方。
function sum(a, b) { return a + b; } var result = sum(3, 4) * sum(2, 6);function sum(a, b) { return a + b; } var result = sum(3, sum(4, 5)); // 函数嵌套
遇见 return
即退出函数。
结合 if 语句的时候,往往不需要写 else 分支了。
// 判断一个数字是否为偶数 function checkEven(n) { if (n % 2 == 0) { return true; } return false; } var result = checkEven(6); console.log(result); // true
7. 全局变量和局部变量
7.1 变量作用域
JS 是函数级作用域编程语言:变量只在其所定义时所在的 function 内部有意义。
重点提示!
注意:JS 是函数级作用域编程语言,没有块级作用域的概念!
即:JS 中 if、switch、for、while 等
()
{}
中的变量都不属于局部变量,而是外层函数的局部变量,如果没有外层函数,那么就是全局变量!注:在 ES6 标准中,JS 引入了块级作用域概念!
7.2 局部变量
function fun(n) { var a = n; } fun(3); console.log(a); // 报错 console.log(n); // 报错
变量 a 是在 fun 函数中被定义的,所以变量 a 只在 fun 函数内部有定义,fun 函数就是 a 的 “作用域”,变量 a 被称为 “局部变量”。
函数形参(n)也属于该函数的作用域!
7.3 全局变量
如果不将变量定义在任何函数的内部(包括形参),此时这个变量就是全局变量,它在任何函数内部都可以被访问和更改。
7.4 遮蔽效应
如果函数中也定义了和全局变量同名的变量,则函数内的局部变量会将全局的变量进行 “遮蔽”。
var a = 10; function fun() { var a = 5; a++; console.log(a); // 6 } fun(); console.log(a); // 10
7.5 注意考虑变量声明提升的情况
7.6 形参也是局部变量
7.7 作用域链
先来认识函数的嵌套:一个函数内部也可以定义一个函数。和局部变量类似,定义在一个函数内部的函数是局部变量。
function fun() { // 局部函数 function inner() { console.log('你好'); } // 调用局部函数 inner(); } // 调用外部函数 fun();
在函数嵌套中,变量会从内到外逐层寻找它的定义(先找本层,本层没有往上一层找,以此类推)。
var a = 10; var b = 20; function fun() { var c = 40; function inner() { var a = 40; var d = 50; console.log(a, b, c, d); // 40 20 40 50 } inner(); } fun();
7.8 不加 var 将定义全局变量
在初次给变量赋值时,如果没有加 var
,则将定义全局变量。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script> // var a = 0; // var a = 2;//替换之前定义的变量 function fun() { a = 3;//替换上面定义的全局变量 } fun(); console.log(a); // 3 </script> </body> </html>
没有特殊情况,一律都要记得加
var
。