数组对象以及函数对象的知识点
一、数组对象
数组是一种特殊的对象。 JS 中的数组跟其他编程语言的数组有很大区别,JS 其实没有真正的数组,只是用对象模拟数组。
JS的数组的特点:
①元素的数据类型可以不同;
②内存不一定是连续的(对象是随机存储的);
③不能通过数字下标获取元素,而是通过字符串下标;( arr[1] 可以取到数组中的元素,是因为JS会调用x.toString() 方法把 1 变成 '1' :arr[(1).toString()] )
④数组可以有任意的 key ,比如:let arr = [1,2,3]; arr['xxx'] = 1
二、创建数组的方法
1. 新建
√ let arr = [1,2,3] //简写,该数组对象的自身属性为:'0'、'1'、'2'、'length'
√ let arr = new Array(1,2,3) //元素为 1、2、3
√ let arr = new Array(3) //长度为3的空数组
2. 转化
√ let arr = str.split(',') //用字符串来创建一个数组(用 , 分隔字符串)
例:
let str = '1,2,3'
str.split(',')
√ let arr =str.split('') //用字符串来创建一个数组(用 空字符串 分隔字符串)
例:
let str = '123'
str.split('')
√ Array.from() //把不是数组的东西尝试变成数组
例1:
Array.from('123')
Array.from({0: 'a', 1: 'b', 2: 'c',length: 3})
注意 :from( )用于将类数组结构转化为数组实例;使用 Array.from( ) 得到数组,第一个参数是一个类数组对象,即任何可迭代的结构,或者要有 length 属性 和 可索引元素 的结构,若可索引元素数量与length不一致,按length值对
3. 合并两个数组,得到新数组
√ arr1.concat(arr2) //会返回一个新数组,且原数组不变
例:
let arr1 = [1,4,5], arr2 = [2,7,8]
arr1.concat(arr2)
4. 截取一个数组的一部分
√ arr.slice(3) //去掉前 3 个元素得到新数组,且不改变原来的数组
例:
let arr = [0,1,2,3,4,5,6]
arr.slice(3)
√ arr.slice(0) //全部截取,不改变原数组
例:
let arr = [5,7,8,1,6,5,2,5,9]
arr.slice(0)
注意: JS 只提供浅拷贝
浅拷贝、深拷贝和赋值的区别: 浅拷贝:也就是拷贝A对象里面的数据,但是不拷贝A对象里面的子对象;深拷贝:会克隆出一个对象,数据相同,但是引用地址不同(就是拷贝A对象里面的数据,而且拷贝它里面的子对象);赋值:获得该对象的引用地址
一个知识点:伪数组
数组对象的原型链会比其他普通对象多一层包括 push、pop 等属性的原型(如果两层原型中有重复的属性,先用前面的),而伪数组的原型链中并没有数组的原型,即,没有数组共用属性的“数组”,叫做伪数组
伪数组可以通过 Array.from() 方法变成数组
三、数组中元素的增删改查
1. 删元素
删除头部元素:
√ arr.shift() //删除并返回第一个元素,arr 被修改
删除尾部元素:
√ arr.pop() //删除并返回最后一个元素,arr 被修改
删除中间的元素:
√ arr.splice(index,1) //删除第 index 起的一个元素,返回被删除的元素
√ arr.splice(index,1,'x') //并在删除位置添加 'x'
√ arr.splice(index,1,'x','y') //并在删除位置添加 'y'
例:
let arr = [1,2,3,4,5,6,7]
arr.splice(3,2) / arr.splice(2,2,666)
如果,删除数组元素和删除对象属性用同样的方法:
例:
let arr = ['a','b','c']
delete arr['0']
显然,数组的第0项为empty,但数组长度没变;那么直接改 length 可以删除元素吗?
例:
let arr = ['a','b','c']
arr.length = 1
可以实现,但不推荐使用此方法。且要注意,不要随便更改数组的 length
2. 查看元素
查看数字(字符串)属性名和值:
√ for (let i = 0; i < arr.length; i++) {console.log(`${i} : ${arr[i]}`) }
√ arr.forEach (function (item,index) {
console.log(`${index} : ${item}`)
})
提示:forEach 的原理是: 用 for 循环访问 array 的每一项,并对每一项调用 fn(array[i], i, array),代码如下
function forEach (array, fn) {
for (let i = 0;i<array.length;i++){
fn(array[i], i, array)
以上两种方式的区别: for 循环里可以使用 break 和continue,而 forEach 不能用
例:
let arr = [1,2,3,4,5,6,7,8,9]
for (let i = 0;i<arr.length;i++){
console.log(`${i} : ${arr[i]}`)
if (i === 3) {
break;
查看单个属性
√ 和查看对象属性一样,用索引查看,例:
let arr = [1,2,3]
arr[0]
注意:不要出现索引越界
arr[arr.length] === undefined / arr[-1] === undefined
例:
let arr = [5,6,7,2,96]
for(let i = 0; i<= arr.length; i++){
console.log(arr[i].toString())
查看某个元素是否在数组里
√ 用 for 循环
let arr = [5,2,3,7,3,8]
for (let i=0;i<arr.length;i++) {
if (i === 3) {
console.log('yes')
√ arr.indexOf(item) //存在返回索引,不存在返回-1
例:
使用条件查找元素 / 元素的索引
√ find / findIndex
arr.find (item => item%2 === 0) //找第一个偶数
arr.findIndex (item => item%2 === 0) //找第一个偶数的索引
3. 增加数组中的元素
在尾部加元素
√ arr.push() //修改arr ,并返回新长度
在头部加元素
√ arr.unshift() //修改arr ,并返回新长度
在中间加元素
√ arr.splice(index,0,'x') //在 index 处,插入 'x'
√ arr.splice(index,0,'x','y') //在 index 处,插入 'x','y'
例:
let arr = [4,5,6]
arr.push(7,8)
arr.unshift(2,3)
arr.splice(1,0,555,66)
4. 修改数组中的元素
√ 用 splice
例:
let arr = [4,5,6]
arr.splice(2,1,5.5)
√ 直接修改
例:arr[3] = 5.5
√ 反转顺序
arr.reverse() //修改原数组
应用:如何把一个字符串反转?
先把字符串变成数组,再把数组反转,再转回字符串
例:
let str = 'abcdefg'
'abcdefg'.split('')
'abcdefg'.split('').reverse()
'abcdefg'.split('').reverse().join('')
√ 自定义顺序
arr.sort((a,b) => a-b)
sort() 方法对数组的元素进行排序,并返回数组。默认排序顺序是在将元素转换为字符串,然后比较它们的UTF-16代码单元值序列时构建的。由于它取决于具体实现,因此无法保证排序的时间和空间复杂性。
例:(JS 中,默认从小到大排)
let arr = [5,2,4,3,1]
arr.sort()
如果不要从小到大排,可以:(JS 中,默认小在前大在后,但是具体谁大谁小是需要 sort 来判断)
let arr =
[5,2,4,3,1]
arr.sort(function(a,b) { //函数接受 a、b 两个值,若 a 大,返回-1,将返回值传给 sort
if(a>b) { //sort 根据值是1/0/-1,来排序
return -1
}else if (a === b) {
return 0
}else {
return 1
四、数组变换
√ map ==>n变n
例:让 [1,2,3,4,5,6] 中每一项都取平方
let arr = [1,2,3,4,5,6]
arr.map(i => i*i)
用 for 循环也可以实现:
let arr = [1,2,3,4,5,6]
for (let i = 0;i<arr.length;i++) {
arr[i] = arr[i]*arr[i]
√ filter ==>n变少
例:提出 [1,2,3,4,5,6] 中所有的偶数
let arr = [1,2,3,4,5,6]
arr.filter(i => i%2 === 0) //对于数组中的每个 i,如果符合要求就留下,不符合就去掉
√ reduce ==>n 变1
例:让 [1,2,3,4,5,6] 中每一项相加求和
let arr = [1,2,3,4,5,6]
let sum = 0
arr.reduce((sum,i) => {return sum + i},0) //0 是 sum 的初始值
用 for 循环:
let arr = [1,2,3,4,5,6]
let sum = 0
for (let i = 0;i<arr.length;i++) {
sum += arr[i] //相当于sum = sum + arr[i]
reduce 可以代替 map 和 filter ,例如:
用 reduce 让 [1,2,3,4,5,6] 中每一项都取平方
let arr = [1,2,3,4,5,6]
arr.reduce((result,i) => {return result.concat(i*i)},[])
用 reduce 提出 [1,2,3,4,5,6] 中所有的偶数
let arr = [1,2,3,4,5,6]
arr.reduce((result,i) => {
if (i%2 === 1) {
return result
}else{
return result.concat(i)
},[])
简化:
let arr = [1,2,3,4,5,6]
arr.reduce((result,i) => i%2 === 1? result: result.concat(i),[])
再简化:
let arr =[1,2,3,4,5,6]
arr.reduce((result,i) => result.concat(i%2 === 1? []: i),[])
五、定义一个函数
函数是对象,所以函数名就是指向函数对象的指针,而且不一定与函数本身紧密绑定。定义一根函数的方式有:
① 函数声明(具名函数)
function sum (x, y) {
return x + y;
说明: 这里,代码定义了一个变量 sum,并将其初始化为一个函数。这个函数可以通过变量 sum 来引用
② 函数表达式(匿名函数)
let a = function (x, y) {
return x + y;
注意: 等号右侧是函数表达式,左侧是声明变量并赋值;
当函数声明和函数表达式合并写时,例如:
let
a = function fn (x, y) {
return x + y;
函数声明在等号右侧,此时 fn 的作用域只在等号右侧部分
③ 箭头函数
let a = (x, y) =>{
return x + y;
注意: 箭头函数中,如果只有一个参数,就可以不用圆括号,只有没有参数、或多个参数的情况下才需要使用圆括号;
箭头函数也可以不使用大括号,但这样会改变函数的行为。使用大括号就说明包含函数体,可以在一个函数中包含多条语句,跟常规的函数一样;如果不使用大括号,箭头后面就只能有一行代码,而且省略大括号会隐式返回这行代码的值
例:
let f1 = x => x*x
f1(9)
let f2 = (x,y) => x * y
f2(8, 9)
let f3 = x => ({name: x}) //要返回一个对象需要加圆括号
f3('Jack')
④ 构造函数(不推荐)
let f = new Function ('x', 'y', 'return x + y')
说明: 构造函数接受任意多字符串参数,最后一个参数始终会被当成函数体,之前的参数都是新函数的参数
作用:可以知道函数是由谁构造的;所有的函数都是 Function 构造的
六、fn 和 fn() 的区别
√ fn 是函数名,使用不带括号的函数名会访问函数指针,而不会执行函数;
√ fn() 是函数调用,会执行函数
例:
let fn = () => console.log('hi')
fn //不会有任何结果,因为 fn 没有执行
fn() //会打印出 'hi'
let fn = () => console.log('hi') //fn 这个变量保存了箭头函数的地址
let fn2 = fn //将箭头函数的地址复制给 fn2
fn2() //调用箭头函数
注意: 函数名就是指向函数的指针,保存了函数的地址,这意味着一个函数可以有多个名称;并且 fn 和 fn2 都只是函数的引用,真正的函数既不是 fn 也不是 fn2
七、函数的要素(每个函数都有)
① 调用时机
时机不同,结果不同
例1:
let a = 1
function fn () {
console.log(a)
fn() //会打印出1
let a = 1
function fn() {
console.log(a)
a = 2 //△
fn() //会打印出2
let a = 1
function fn() {
console.log(a)
fn() //会打印出1
a = 2 //△
例2:
let i = 1
function fn() {
setTimeout(() => {
console.log(i)
},0)
fn() //打印出2
i = 2
let i = 0
for (i = 0; i<6; i++) {
setTimeout(() => {
console.log(i)
},0)
}
//会打印出 6 个 6
for (let i = 0; i<6; i++) {
setTimeout(() => {
console.log(i)
},0)
} //打印出 0,1,2,3,4,5
注意: 打印出 6 个 6 还是 0,1,2,3,4,5,区别在 let 在 for 循环的里面还是外面,详细分析见下一篇文章
② 作用域(每个函数都会默认创建一个作用域)
例:
function fn () {
let a = 1
fn()
console.log(a) //a 不存在
注意: a 的作用域只在 fn 里面,出了这个作用域就不存在
一个知识点: 全局变量和局部变量
√ 在顶级作用域声明的变量、以及 window 的属性是全局变量
√ 其他变量都是局部变量
window 的属性,例如 parseInt、Object 等,都可以直接用;另外,还可以把变量写在 window 上,例如,window.c = 2,这样不管写在哪都可以直接用
函数可以进行嵌套(作用域也可以嵌套),例:
function f1 () {
let a = 1
function f2 () {
let a = 2
console.log(a) //a = 2
console.log(a) //a = 1
a = 3
f2()
f1()
注意: 结合上图,a=1 的作用域在绿色线圈内,a=2 的作用域在红色线圈内,所以会打印出1、2
总结:作用域规则:
如果多个作用域有同名变量 a ,查找 a 的声明时就向上取最近的作用域,简称就近原则。
查找 a 的过程与函数执行无关,但 a 的值与函数执行有关
③ 闭包
定义: 如果一个函数用到了外部的变量,那么这个函数加这个变量就叫做闭包
例:
function f1 () {
let a = 1
function f2() {
let a = 2
function f3 () {
console.log(a) //f3 中没有 a,按照就近原则向上找,找到“let a=2”,然后再确定 a 的值,为22
a = 22
f3()
console.log(a) //a = 1
a = 100
f2()
f1()
这段代码中,f3 用到了外面的 变量 a,那么 a 和 f3 就组成了一个闭包
④ 形式参数(即 非实际参数)
例:
function add (x, y){ //x 和 y 就是形式参数,而不是实际参数
return x + y
add(1, 2) //调用 add 时,1 和 2 是实际参数,会被赋值给x、y
注意: ECMAScript 中所有的参数都按值传递,不可能按引用传递参数。如果把对象作为参数传递,那么传递的值就是这个对象的引用
形参的本质就是变量声明,上面的代码等价于:
function add (){
var x = arguments[0]
var y = arguments[1]
return x + y
add(1, 2)
形参可多可少(形参只是给参数取名字)
ECMAScript 函数既不关心传入的参数个数,也不关心这些参数的数据类型。定义函数时,要接收两个参数,不一定调用时就要传两个参数,传一个、三个甚至不传都不会报错。
原因是 ECMAScript 函数的参数在内部表现为一个数组,函数被调用时,总会接收一个数组,但函数并不关心这个数组包含什么,如果数组中什么也没有或者数组的元素超出了要求,都没问题。事实上,在使用 function 关键字定义(非箭头)函数时,可以在函数内部访问 arguments 对象,从中取得传进来的每个参数值。
例:
function add (x){
return x + arguments[1]
add(1, 2) //3
⑤ 返回值
√ 每个函数都有返回值,且只有函数有返回值
例:1+2 的返回值为 3(说法错误) 1+2 的值为 3(对)
√ 函数执行完之后才会返回
例:
function hi () {
console.log('hi') //没写 return,所以返回值是 undefined
hi()
function hi () {
return console.log('hi') //返回值是 console.log('hi') 的值,即undefined
hi()
⑥ 调用栈
JS 引擎在调用一个函数前需要把函数所在的环境 push 到一个数组里,这个数组叫调用栈。等函数执行完了,就会把环境弹(pop)出来,然后 return 到之前的环境,继续执行后续代码
例:
递归函数(有可能把调用栈压满):
通常的形式是一个函数通过名称调用自己
例:
调用栈最长有多少?可以用以下代码测试
function computeMaxCallStackSize() {
try {
return 1 + computeMaxCallStackSize();
} catch (e) { // stack overflow
return 1;
⑦ 函数提升
定义: 函数声明会在任何代码执行之前先被读取并添加到执行上下文,这个过程叫函数声明提升。
JS 引擎在加载数据时对函数声明和函数表达式是区别对待的。JS 引擎在任何代码执行前,先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义
例:
f1(1, 2)
function f1 (x,y){
return x+y
注意: let fn = function f1 () {} 不会提升
⑧ arguments 和 this(每个函数都有,除了箭头函数)
函数内部存在两个特殊的对象:arguments 和 this
- arguments :arguments 是一个伪数组,包含了调用函数时传入的所有参数;在使用 function 关键字定义(非箭头)函数时,可以在函数内部访问arguments 对象,从中取得传进来的每个参数值
代码:
function fn (){
console.log(arguments)
调用 fn 即可传 arguments
fn(1,2,3),则 arguments 就是包含 [1,2,3] 的伪数组
- this :
function fn (){
console.log(this)
如果不给任何条件,this 默认指向 window
目前可以用 fn.call(xxx,1,2,3) 传 this 和 arguments, 且当传的 this 不是对象,JS 会自动将其封装成对象
理解: this 是隐藏参数,arguments 是普通参数
在标准函数中,this 引用的是把函数当成方法调用的上下文对象,this 到底引用哪个对象必须到函数被调用时才能确定
JS 提供两种调用形式:
√ person.sayHi() //会自动把 person 传到函数里,作为 this
√ person.sayHi.call(person) //需要自己手动把 person 传到函数里,作为 this
手动传:例:
function add (x,y){return x+y}
add.call(undefined,1,2) //第一个参数要作为 this,但代码中没用到 this,只能用undefined/null占位
Array.prototype.forEach2 = function (fn) {
for (let i = 0;i<this.length;i++){
fn(this[i],i,this)
let array = [1,2,3]
array.forEach.call(array,(item) => console.log(item))
总结: this 的两种使用方法:
√ 隐式传递:
fn(1,2) 等价于 fn.call(undefined,1,2)
obj.child.fn(1) 等价于 obj.child.fn.call(obj.child,1)
√ 显示传递:
fn.call(undefined,1,2)
fn.apply(undefined,[1,2])
八、绑定 this
- 使用 .bind 可以让 this 不被改变
例:
function f1 (p1,p2) {
console.log(this,p1,p2)