Skip to content

JavaScript进阶

内容概述

这一讲先初步了解对象,理解原始值和引用值区分,然后深入函数和数组

教学目的

  • 掌握对象的创建方式,理解原始值和引用值的区别
  • 掌握箭头函数的书写
  • 掌握函数agruments、默认参数、参数扩展与收集(...操作符)
  • 理解高阶函数
  • 理解this对象
  • 理解函数作用域和作用域链
  • 理解递归
  • 掌握数组的检测方式和数组的常用方法

具体内容

  • 对象基础:理解对象、原始值和引用值
  • 函数进阶:箭头函数、函数名、参数相关、高阶函数、this、函数作用域、递归
  • 数组进阶:检测数组、常用方法(indexOf、find、findIndex、includes、sort、reverse、concat、join、slice、every、some、filter、forEach、map)

对象基础

理解对象

JavaScript将对象定义为一组属性的无序集合。严格来说,这意味着对象就是一组没有特定顺序的值。对象的每个属性或方法都由一个名称来标识,这个名称映射到一个值。正因为如此(以及其他还未讨论的原因),可以把 JavaScript 的对象想象成一张散列表,其中的内容就是一组名/值对,值可以是数据或者函数。

  1. 对象字面量
javascript
let person = { 
 "name": "hello", 
 "age": 29
};
  1. Object构造函数
javascript
let person = new Object(); 
person.name = "hello"; 
person.age = 29;

原始值和引用值

​ JavaScript 变量可以包含两种不同类型的数据:原始值引用值。原始值(primitive value)就是最简单的数据,引用值(reference value)则是由多个值构成的对象。

​ 在把一个值赋给变量时,JavaScript 引擎必须确定这个值是原始值还是引用值。保存原始值的变量是按值访问的,因为我们操作的就是存储在变量中的实际值。

​ 引用值是保存在内存中的对象。与其他语言不同,JavaScript 不允许直接访问内存位置,因此也就不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的引用而非实际的对象本身。为此,保存引用值的变量是按引用访问的。

  1. 动态属性

    原始值和引用值的定义方式很类似,都是创建一个变量,然后给它赋一个值。不过,在变量保存了这个值之后,可以对这个值做什么,则大有不同。对于引用值而言,可以随时添加、修改和删除其属性和方法。比如,看下面的例子:

javascript
let person = new Object(); 
person.name = "hello"; 
console.log(person.name); // "hello"

这里,首先创建了一个对象,并把它保存在变量 person 中。然后,给这个对象添加了一个名为name 的属性,并给这个属性赋值了一个字符串"Nicholas"。在此之后,就可以访问这个新属性,直到对象被销毁或属性被显式地删除。

​ 原始值不能有属性,尽管尝试给原始值添加属性不会报错。比如:

javascript
let name = "hello"; 
name.age = 27; 
console.log(name.age); // undefined
  1. 复制值

​ 除了存储方式不同,原始值和引用值在通过变量复制时也有所不同。在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置。请看下面的例子:

javascript
let num1 = 5; 
let num2 = num1;

这里,num1 包含数值 5。当把 num2 初始化为 num1 时,num2 也会得到数值 5。这个值跟存储在num1 中的 5 是完全独立的,因为它是那个值的副本。

原始值复制

在把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。区别在于,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。操作完成后,两个变量实际上指向同一个对象,因此一个对象上面的变化会在另一个对象上反映出来,如下面的例子所示:

javascript
let obj1 = new Object(); 
let obj2 = obj1; 
obj1.name = "hello"; 
console.log(obj2.name); // "hello"

  1. 传递参数

JavaScript 中所有函数的参数都是按值传递的。这意味着函数外的值会被复制到函数内部的参数中,就像从一个变量复制到另一个变量一样。如果是原始值,那么就跟原始值变量的复制一样,如果是引用值,那么就跟引用值变量的复制一样。

javascript
function addTen(num) { 
 num += 10; 
 return num; 
} 
let count = 20;
let result = addTen(count); 
console.log(count); // 20,没有变化
console.log(result); // 30

function setName(obj) { 
 obj.name = "hello"; 
} 
let person = new Object(); 
setName(person); 
console.log(person.name); // "hello"

函数进阶

箭头函数

ECMAScript 6 新增了使用胖箭头(=>)语法定义函数表达式的能力。很大程度上,箭头函数实例化的函数对象与正式的函数表达式创建的函数对象行为是相同的。任何可以使用函数表达式的地方,都可以使用箭头函数:

javascript
let arrowSum = (a, b) => { 
 return a + b; 
}; 
let functionExpressionSum = function(a, b) { 
 return a + b; 
}; 
console.log(arrowSum(5, 8)); // 13 
console.log(functionExpressionSum(5, 8)); // 13

如果只有一个参数,那也可以不用括号。只有没有参数,或者多个参数的情况下,才需要使用括号:

javascript
// 以下两种写法都有效
let double = (x) => { return 2 * x; }; 
let triple = x => { return 3 * x; }; 
// 没有参数需要括号
let getRandom = () => { return Math.random(); }; 
// 多个参数需要括号
let sum = (a, b) => { return a + b; }; 
// 无效的写法:
let multiply = a, b => { return a * b; };

箭头函数也可以不用大括号,但这样会改变函数的行为。使用大括号就说明包含“函数体”,可以在一个函数中包含多条语句,跟常规的函数一样。如果不使用大括号,那么箭头后面就只能有一行代码,比如一个赋值操作,或者一个表达式。而且,省略大括号会隐式返回这行代码的值:

javascript
// 以下两种写法都有效,而且返回相应的值
let double = (x) => { return 2 * x; }; 
let triple = (x) => 3 * x; 
// 可以赋值
let value = {}; 
let setName = (x) => x.name = "Matt"; 
setName(value); 
console.log(value.name); // "Matt" 
// 无效的写法:
let multiply = (a, b) => return a * b;

函数名

在JavaScript里面函数作为一种值可以赋值给多个变量,这意味着一个函数可以有多个名称,而函数名就是指向函数的指针。

javascript
function sum(num1, num2) { 
 return num1 + num2; 
} 
console.log(sum(10, 10)); // 20 
let anotherSum = sum; 
console.log(anotherSum(10, 10)); // 20 
sum = null; 
console.log(anotherSum(10, 10)); // 20

参数

JavaScript 函数的参数跟大多数其他语言不同。JavaScript 函数既不关心传入的参数个数,也不关心这些参数的数据类型。定义函数时要接收两个参数,并不意味着调用时就传两个参数。你可以传一个、三个,甚至一个也不传,解释器都不会报错。

之所以会这样,主要是因为 JavaScript 函数的参数在内部表现为一个数组。函数被调用时总会接收一个数组,但函数并不关心这个数组中包含什么。如果数组中什么也没有,那没问题;如果数组的元素超出了要求,那也没问题。

agruments

在使用 function 关键字定义(非箭头)函数时,可以在函数内部访问 arguments 对象,从中取得传进来的每个参数值。

arguments 对象是一个类数组对象(但不是 Array 的实例),因此可以使用中括号语法访问其中的元素(第一个参数是 arguments[0],第二个参数是 arguments[1])。而要确定传进来多少个参数,可以访问 arguments.length 属性。

javascript
function sayHi(name, message) { 
 console.log("Hello " + name + ", " + message); 
}
javascript
function sayHi() { 
 console.log("Hello " + arguments[0] + ", " + arguments[1]); 
}

默认参数

​ 在 ECMAScript5.1 及以前,实现默认参数的一种常用方式就是检测某个参数是否等于 undefined,如果是则意味着没有传这个参数,那就给它赋一个值:

javascript
function say(name) {
  name = (typeof name !== 'undefined') ? name : 'world';
  return 'hello ' + name;
}
console.log(say());
console.log(say('js'));

​ 在使用默认参数时,arguments 对象的值不反映参数的默认值,只反映传给函数的参数。

javascript
function say(name = 'world') {
  return `hello ${arguments[0]}`;
}
console.log(say());
console.log(say('js'));

参数扩展与收集

ECMAScript 6 新增了扩展操作符,使用它可以非常简洁地操作和组合集合数据。扩展操作符最有用的场景就是函数定义中的参数列表,在这里它可以充分利用这门语言的弱类型及参数长度可变的特点。扩展操作符既可以用于调用函数时传参,也可以用于定义函数参数。

  • 扩展参数

在给函数传参时,有时候可能不需要传一个数组,而是要分别传入数组的元素。假设有如下函数定义,它会将所有传入的参数累加起来:

javascript
function getSum() { 
 let sum = 0; 
 for (let i = 0; i < arguments.length; ++i) { 
 sum += arguments[i]; 
 } 
 return sum; 
}
let values = [1, 2, 3, 4];

在 ECMAScript 6 中,可以通过扩展操作符极为简洁地实现这种操作。对可迭代对象应用扩展操作符,并将其作为一个参数传入,可以将可迭代对象拆分,并将迭代返回的每个值单独传入。比如,使用扩展操作符可以将前面例子中的数组像这样直接传给函数:

javascript
console.log(getSum(...values)); // 10

因为数组的长度已知,所以在使用扩展操作符传参的时候,并不妨碍在其前面或后面再传其他的值,包括使用扩展操作符传其他参数:

javascript
console.log(getSum(-1, ...values)); // 9 
console.log(getSum(...values, 5)); // 15 
console.log(getSum(-1, ...values, 5)); // 14 
console.log(getSum(...values, ...[5,6,7])); // 28
  • 收集参数

    在构思函数定义时,可以使用扩展操作符把不同长度的独立参数组合为一个数组。这有点类似arguments 对象的构造机制,只不过收集参数的结果会得到一个Array 实例。

javascript
function getSum(...values) {
  // 顺序累加 values 中的所有值
  let sum = 0;
  for (let i = 0; i < values.length; ++i) {
    sum += values[i];
  }
  return sum;
}
console.log(getSum(1,2,3)); // 6

收集参数的前面如果还有命名参数,则只会收集其余的参数;如果没有则会得到空数组。因为收集参数的结果可变,所以只能把它作为最后一个参数:

javascript
// 不可以
function getProduct(...values, lastValue) {} 
// 可以
function ignoreFirst(firstValue, ...values) { 
 console.log(values); 
}

高阶函数

高阶函数是函数式编程中的一个核心概念,它允许你将函数作为一等公民来对待,即函数可以像变量一样被传递、赋值、作为参数或返回值。

匿名函数

JavaScript 中的匿名函数是没有名称的函数。它们通常被用于回调函数、立即执行函数表达式(IIFE)、以及作为参数传递给其他函数。

函数作为参数

接受一个或多个函数作为参数:这意味着你可以将一个函数作为参数传递给另一个函数。

javascript
// 遍历数组
let arr = [1, 2, 3, 4, 5]
arr.forEach(function (item) {
    console.log(item)
})

函数作为返回值

函数执行完毕后,返回一个新的函数。

javascript
function makeAdder(x) {  
  return function(y) {  
    return x + y;  
  };  
}  
  
const add5 = makeAdder(5);  
console.log(add5(2)); // 输出 7  
  
const add10 = makeAdder(10);  
console.log(add10(20)); // 输出 30

函数内部

this

在函数内部可以访问到一个特殊对象this,它在标准函数和箭头函数中有不同的行为。

在标准函数中,this 引用的是把函数当成方法调用的上下文对象,这时候通常称其为 this 值(在网页的全局上下文中调用函数时,this 指向 windows)。来看下面的例子:

javascript
window.color = 'red'; 
let o = { 
 color: 'blue' 
}; 
function sayColor() { 
 console.log(this.color); 
} 
sayColor(); // 'red' 
o.sayColor = sayColor; 
o.sayColor(); // 'blue'

在箭头函数中,this引用的是定义箭头函数的上下文。下面的例子演示了这一点。在对sayColor()的两次调用中,this 引用的都是 window 对象,因为这个箭头函数是在 window 上下文中定义的:

javascript
window.color = 'red'; 
let o = { 
 color: 'blue' 
}; 
let sayColor = () => console.log(this.color); 
sayColor(); // 'red' 
o.sayColor = sayColor; 
o.sayColor(); // 'red'

函数作用域

作用域链

在JavaScript中,函数作用域链(Scope Chain)是一个非常重要的概念,它决定了函数内部如何查找变量。理解作用域链对于编写高质量的JavaScript代码至关重要。

作用域链是JavaScript中一种通过对象链查找变量的机制。当JavaScript引擎需要查找一个变量时,它会从当前执行环境的变量对象中开始查找。如果当前执行环境的变量对象中找不到该变量,它就会向上移动到父执行环境的变量对象中继续查找,直到找到该变量或到达全局执行环境(即全局对象)为止。这个查找过程就是作用域链的工作方式。

对于函数来说,其作用域链是在函数定义时创建的,而不是在函数调用时。函数的作用域链由两部分组成:

  1. 函数内部作用域:函数内部的局部变量、命名参数以及通过this引入的对象都会作为属性添加到函数自身的变量对象中。这个变量对象在函数被调用时创建,并在函数执行完成后销毁。
  2. 父级作用域链:在函数变量对象之后,就是包含该函数的外部作用域(即父级作用域)的变量对象。这个链会一直向上延伸到全局作用域(在浏览器中是window对象,在Node.js中是global对象)。
javascript
let x = 1;  
  
function outer() {  
    let x = 2;  
  
    function inner() {  
        let x = 3;  
        console.log(x); // 查找并打印当前作用域中的x,即3  
    }  
  
    inner();  
  
    console.log(x); // 查找并打印outer函数作用域中的x,即2  
}  
  
outer();  
console.log(x); // 查找并打印全局作用域中的x,即1

递归

javascript
function factorial(num) { 
 if (num <= 1) { 
 return 1; 
 } else { 
 return num * factorial(num - 1); 
 } 
}

arguments.callee 就是一个指向正在执行的函数的指针,因此可以在函数内部递归调用,如下所示:

javascript
function factorial(num) { 
 if (num <= 1) { 
 return 1; 
 } else { 
 return num * arguments.callee(num - 1); 
 } 
}

数组进阶

检测数组

  1. instanceof 操作符
javascript
if (value instanceof Array){ 
 // 操作数组
}
  1. Array.isArray()
javascript
if (Array.isArray(value)){ 
 // 操作数组
}

搜索元素

  • indexOf():返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。
javascript
let arr = [1, 2, 3]
console.log(arr.indexOf(3)); // 2
console.log(arr.indexOf(4)); // -1
  • find():返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。
javascript
// 找到第一个能被3整除的数
let arr = [1, 2, 3, 4]
const num = arr.find(function (item) {
  return item % 3 === 0
})
console.log(num);
  • findIndex():返回数组中满足提供的测试函数的第一个元素的索引。否则返回-1。
javascript
// 找到第一个能被3整除的数的索引
let arr = [1, 2, 3, 4]
const num = arr.findIndex(function (item) {
  return item % 3 === 0
})
console.log(num);
  • includes():用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回 false。
javascript
// 找到第一个能被3整除的数的索引
let arr = [1, 2, 3, 4]
console.log(arr.includes(4)); // true
console.log(arr.includes(5)); // false

排序和反转

  • sort():对数组的元素进行排序,并返回数组。

默认情况下,sort()会按照升序重新排列数组元素,即最小的值在前面,最大的值在后面。为此,sort()会在每一项上调用 String()转型函数,然后比较字符串来决定顺序。即使数组的元素都是数值,也会先把数组转换为字符串再比较、排序。比如:

javascript
let values = [0, 1, 5, 10, 15]; 
values.sort(); 
console.log(values); // 0,1,10,15,5

比较函数

很明显,在多数情况下用字符串来排序都不是最合适的。为此,sort()方法可以接收一个比较函数,用于判断哪个值应该排在前面。

比较函数接收两个参数,如果第一个参数应该排在第二个参数前面,就返回负值;如果两个参数相等,就返回 0;如果第一个参数应该排在第二个参数后面,就返回正值。

javascript
function compare(value1, value2) {
  if (value1 < value2) {
    return -1;
  } else if (value1 > value2) {
    return 1;
  } else {
    return 0;
  }
}

let values = [0, 1, 5, 10, 15]; 
values.sort(compare); 
console.log(values); // 0,1,5,10,15

当然,比较函数也可以产生降序效果,只要把返回值交换一下即可

javascript
function compare(value1, value2) {
  if (value1 < value2) {
    return 1;
  } else if (value1 > value2) {
    return -1;
  } else {
    return 0;
  }
}
let values = [0, 1, 5, 10, 15];
values.sort(compare);
console.log(values); // 15,10,5,1,0

如果数组的元素是数值,这个比较函数还可以写得更简单,因为这时可以直接用第二个值减去第一个值:

javascript
function compare(value1, value2){ 
 return value2 - value1; 
}

let values = [0, 1, 5, 10, 15];
values.sort(compare);
console.log(values); // 15,10,5,1,0
  • reverse():颠倒数组中元素的顺序,并返回该数组。
javascript
let values = [1, 2, 3, 4, 5]; 
values.reverse(); 
console.log(values); // 5,4,3,2,1

连接和分割

  • concat():用于合并两个或多个数组。此方法不会改变现有的数组,而仅仅会返回被合并数组的一个副本。
javascript
let arr = [1, 2, 3]
let newArr = arr.concat([4, 5, 6])
console.log(newArr);
console.log(arr);
  • join():把数组的所有元素放入一个字符串。元素通过指定的分隔符进行分隔。
javascript
let arr = [1, 2, 3]
console.log(arr.join()); // 1,2,3
console.log(arr.join("|")); // 1|2|3

截取

slice()

slice()用于创建一个包含原有数组中一个或多个元素的新数组。slice()方法可以接收一个或两个参数:返回元素的开始索引和结束索引。如果只有一个参数,则 slice()会返回该索引到数组末尾的所有元素。如果有两个参数,则 slice()返回从开始索引到结束索引对应的所有元素,其中不包含结束索引对应的元素。记住,这个操作不影响原始数组。来看下面的例子:

javascript
let colors = ["red", "green", "blue", "yellow", "purple"]; 
let colors2 = colors.slice(1); 
let colors3 = colors.slice(1, 4); 
alert(colors2); // green,blue,yellow,purple 
alert(colors3); // green,blue,yellow

迭代方法

JavaScript 为数组定义了 5 个迭代方法。每个方法接收两个参数:以每一项为参数运行的函数,以及可选的作为函数运行上下文的作用域对象(影响函数中 this 的值)。传给每个方法的函数接收 3个参数:数组元素、元素索引和数组本身。因具体方法而异,这个函数的执行结果可能会也可能不会影响方法的返回值。数组的 5 个迭代方法如下。

  • every():对数组每一项都运行传入的函数,如果对每一项函数都返回 true,则这个方法返回 true。
javascript
// 判断数组中元素是否全是偶数
let arr1 = [2, 4, 6]
let arr2 = [1, 2, 4]
console.log(arr1.every(item => item % 2 === 0)); // true
console.log(arr2.every(item => item % 2 === 0)); // false
  • some():对数组每一项都运行传入的函数,如果有一项函数返回 true,则这个方法返回 true。
javascript
// 判断数组中元素是否全是偶数
let arr1 = [2, 4, 6]
let arr2 = [1, 3, 5]
console.log(arr1.some(item => item % 2 === 0)); // true
console.log(arr2.some(item => item % 2 === 0)); // false
  • filter():对数组每一项都运行传入的函数,函数返回 true 的项会组成数组之后返回。
javascript
// 从数组中筛选出奇数
let arr = [1, 2, 3, 4, 5]
let newArr = arr.filter(item => item % 2 !== 0)
console.log(newArr);
  • forEach():对数组每一项都运行传入的函数,没有返回值。
javascript
// 遍历数组
let arr = [1, 2, 3, 4, 5]
arr.forEach(item => {
  console.log(item);
})
  • map():对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组。
javascript
// 计算每个数的平方返回一个新的数组
let arr = [1, 2, 3, 4, 5]
let newArr = arr.map(item => {
  return item * item
})
console.log(newArr);

归并方法

JavaScript 为数组提供了两个归并方法:reduce()reduceRight()。这两个方法都会迭代数组的所有项,并在此基础上构建一个最终返回值。reduce()方法从数组第一项开始遍历到最后一项。而 reduceRight()从最后一项开始遍历至第一项。

这两个方法都接收两个参数:对每一项都会运行的归并函数,以及可选的以之为归并起点的初始值。传给 reduce()和 reduceRight()的函数接收 4 个参数:上一个归并值、当前项、当前项的索引和数组本身。这个函数返回的任何值都会作为下一次调用同一个函数的第一个参数。如果没有给这两个方法传入可选的第二个参数(作为归并起点值),则第一次迭代将从数组的第二项开始,因此传给归并函数的第一个参数是数组的第一项,第二个参数是数组的第二项。

javascript
let values = [1, 2, 3, 4, 5];
let sum = values.reduce((prev, cur, index, array) => prev + cur, 0);
console.log(sum); // 15

作业

  1. 将下列函数转换为箭头函数
javascript
function getSum (a, b) {
  return a + b;
}

function square (a) {
  return a * a;
}

function say (name, message) {
  console.log(name);
  console.log(message);
  console.log(name + ':' + message)
}
  1. 实现函数test1,参数接收一个数组表示所有学生考试分数,数组每个元素是个对象,对象有两个属性id和score分数,筛选出分数大于等于60的学生然后求平均分(取整数部分)
javascript
function test1 (arr) {
  // 实现代码
}

// 参数示例
const students = [
  {
    id: 1,
    score: 90
  },
  {
    id: 2,
    score: 45
  },
  {
    id: 3,
    score: 70
  },
  {
    id: 4,
    score: 65
  },
  {
    id: 5,
    score: 55
  }
]

console.log(test1(students)); // 75
  1. 实现函数test2,参数接收一个数组表示所有学生考试分数,数组每个元素是个对象,对象有两个属性id和score分数,去掉一个最低分,去掉一个最高分,然后求平均分(取整数部分)
javascript
function test2 (arr) {
  // 实现代码
}

// 参数示例
const students = [
  {
    id: 1,
    score: 90
  },
  {
    id: 2,
    score: 45
  },
  {
    id: 3,
    score: 70
  },
  {
    id: 4,
    score: 65
  },
  {
    id: 5,
    score: 55
  }
]

console.log(test2(students)); // 63