数组对象以及函数对象的知识点

一、数组对象

数组是一种特殊的对象。 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)