2.1 函数进阶
2.1.1 高阶函数
JavaScript 中的高阶函数是指可以接受一个或多个函数作为参数,并/或者返回一个函数的函数。这种函数可以用来实现许多有用的编程模式,如函数式编程和回调函数。
以下是一些常见的 JavaScript 高阶函数示例:
map()
函数:接受一个函数作为参数,并将该函数应用于数组的每个元素,并返回一个新的经过函数处理的数组。
const numbers = [1, 2, 3, 4, 5];
const squaredNumbers = numbers.map(function (num) {return num * num;
});
console.log(squaredNumbers); // 输出:[1, 4, 9, 16, 25]
filter()
函数:接受一个函数作为参数,并使用该函数对数组进行筛选,返回一个包含满足条件的元素的新数组。
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(function (num) {return num % 2 === 0;
});
console.log(evenNumbers); // 输出:[2, 4]
reduce()
函数:接受一个函数作为参数,并使用该函数将数组的元素归约为单个值。
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce(function (acc, num) {return acc + num;
}, 0);
console.log(sum); // 输出:15
forEach()
函数:接受一个函数作为参数,并对数组的每个元素执行该函数。
const numbers = [1, 2, 3, 4, 5];
numbers.forEach(function (num) {console.log(num);
});
// 输出:
// 1
// 2
// 3
// 4
// 5
这些只是高阶函数的一些示例,JavaScript 中还有许多其他的高阶函数,如sort()
、every()
、some()
等,它们都能提供更多的功能和灵活性,以便在 JavaScript 中实现复杂的逻辑和算法。
2.1.2 闭包和作用域链
闭包(Closure)和作用域链(Scope Chain)是 JavaScript 中重要的概念,它们密切相关并经常一起讨论。
作用域(Scope)是指变量、函数和对象的可访问范围。在 JavaScript 中,每个函数都会创建一个新的作用域。作用域链是指在函数嵌套的情况下,内部函数可以访问外部函数的变量和函数,形成一个作用域的链条。
闭包是指在函数内部创建的函数,并且可以访问其外部函数的变量和作用域,即使外部函数已经执行完毕,闭包仍然保留对外部函数作用域的引用。闭包使得函数可以记住并访问在其词法作用域之外的变量。
下面是一个示例来说明闭包和作用域链的概念:
function outerFunction() {var outerVariable = "I am outside!";function innerFunction() {console.log(outerVariable); // 内部函数可以访问外部函数的变量}return innerFunction;
}var myFunction = outerFunction();
myFunction(); // 输出:'I am outside!'
在上面的例子中,innerFunction
是在outerFunction
内部定义的,并且可以访问outerFunction
的变量outerVariable
。尽管outerFunction
已经执行完毕,但通过返回innerFunction
,我们创建了一个闭包,innerFunction
仍然可以访问和引用外部作用域中的变量。
作用域链是由函数的定义位置和调用位置共同决定的。当访问变量时,JavaScript 引擎首先在当前作用域中查找,如果找不到,则继续沿着作用域链向上查找,直到找到变量或到达全局作用域。这种机制保证了内部函数可以访问外部函数的变量。
闭包和作用域链在 JavaScript 中具有广泛的应用,可以用于封装私有变量、实现模块化、处理异步操作等场景。但同时需要注意避免内存泄漏,因为闭包会持有外部作用域的引用,导致外部作用域中的变量无法被垃圾回收。
2.1.3 函数式编程
函数式编程是一种编程范式,它将计算视为函数应用的连续转换和组合,强调使用纯函数和避免可变状态和副作用。在 JavaScript 中,函数式编程可以通过以下几个方面来实现:
- 纯函数(Pure Functions):纯函数是指在相同的输入下,总是产生相同的输出,并且没有副作用的函数。它们不依赖于外部状态,也不修改外部状态。纯函数对于给定的输入只关心输出,不产生额外的影响。
function square(x) {return x * x;
}const result = square(5); // 纯函数调用
- 不可变数据(Immutable Data):在函数式编程中,数据是不可变的,即一旦创建,就不能修改。当需要修改数据时,实际上是创建一个新的数据副本,并在副本上进行操作。这有助于避免意外的副作用和数据竞争。
const numbers = [1, 2, 3, 4, 5];
const doubledNumbers = numbers.map((num) => num * 2); // 创建新的数组副本
- 高阶函数(Higher-order Functions):高阶函数是指可以接受一个或多个函数作为参数,并/或者返回一个函数的函数。它们可以用来组合和转换函数,使代码更具表现力和可重用性。
function multiplyBy(factor) {return function (x) {return x * factor;};
}const triple = multiplyBy(3); // 返回一个函数
const result = triple(5); // 调用返回的函数
- 函数组合(Function Composition):函数组合是将多个函数组合成一个新的函数的过程。可以使用函数组合运算符(如
compose
或pipe
)将多个函数按特定顺序组合起来,形成一个新的函数。
const add = (x) => x + 1;
const multiply = (x) => x * 2;const combined = compose(multiply, add); // 组合函数
const result = combined(3); // 调用组合后的函数
- 延迟执行(Lazy Evaluation):函数式编程鼓励延迟执行,即在需要时才计算结果。这可以通过使用惰性计算、生成器、迭代器等技术来实现,以避免不必要的计算。
function* generateNumbers() {let i = 0;while (true) {yield i++;}
}const numbers = generateNumbers(); // 创建生成器
const result = numbers.next().value; // 惰性计算,只计算下一个值
函数式编程提供了一种声明式的编程风格,强调代码的可读性、可维护性和可测试性。它可以使代码更加模块化、可组合和
易于推理。在 JavaScript 中,许多库(如 Lodash、Ramda)提供了丰富的函数式编程工具和函数,可以更方便地编写函数式风格的代码。
2.2 对象和原型
2.2.1 对象创建和构造函数
在 JavaScript 中,对象可以通过多种方式进行创建。以下是两种常见的方式:字面量对象创建和构造函数创建。
- 字面量对象创建:
使用字面量语法直接创建对象,可以在大括号{}
内指定对象的属性和方法。
// 创建一个空对象
const obj = {};// 创建带有属性和方法的对象
const person = {name: "John",age: 30,greet: function () {console.log("Hello!");},
};
- 构造函数创建:
使用构造函数创建对象,构造函数是一个普通的函数,通过new
关键字调用,并在函数内部使用this
关键字引用新创建的对象。
// 创建一个构造函数
function Person(name, age) {this.name = name;this.age = age;this.greet = function () {console.log("Hello!");};
}// 使用构造函数创建对象
const person = new Person("John", 30);
在上面的示例中,Person
是一个构造函数,通过 new
关键字创建了一个 Person
类型的对象,并传递了相应的参数。构造函数内部使用 this
关键字来引用新创建的对象,并设置对象的属性和方法。
使用构造函数创建的对象具有相同的属性和方法,因此它们共享相同的原型。可以通过在构造函数的原型对象上定义属性和方法,实现对象之间的共享和原型继承。
// 在构造函数原型上定义方法
Person.prototype.introduce = function () {console.log(`My name is ${this.name} and I'm ${this.age} years old.`);
};// 调用构造函数原型上的方法
person.introduce(); // 输出:My name is John and I'm 30 years old.
通过使用构造函数和原型,可以实现更好的代码复用和扩展性,使对象拥有共享的属性和方法,同时每个对象也可以具有自己的属性和方法。
2.2.2 原型和原型链
在 JavaScript 中,每个对象都有一个原型(prototype),并且原型之间可以形成一个原型链(prototype chain)。原型链是 JavaScript 实现继承的机制之一。
- 原型(prototype):
- 每个 JavaScript 对象都有一个原型,它可以是另一个对象或 null。
- 对象的原型可以通过
__proto__
属性访问(不推荐使用)或使用Object.getPrototypeOf(obj)
方法获取。 - 原型是对象共享属性和方法的存储位置,当对象访问属性或方法时,如果对象本身没有该属性或方法,它会沿着原型链向上查找。
- 原型链(prototype chain):
- 原型链是由对象的原型构成的链条,每个对象都有一个指向其原型的链接。
- 当对象访问属性或方法时,如果对象本身没有该属性或方法,它会沿着原型链向上查找,直到找到该属性或方法或到达原型链的末端(null)。
- 原型链的顶端是
Object.prototype
,它是大多数对象的最终原型。
以下是一个示例,说明原型和原型链的概念:
// 定义一个构造函数
function Person(name) {this.name = name;
}// 在构造函数原型上定义方法
Person.prototype.greet = function () {console.log(`Hello, my name is ${this.name}.`);
};// 创建对象实例
const person = new Person("John");// 访问对象属性
console.log(person.name); // 输出:John// 访问对象方法
person.greet(); // 输出:Hello, my name is John.// 通过原型链访问对象方法
console.log(person.toString()); // 输出:[object Object]
在上面的例子中,person
对象实例通过原型链继承了 Object.prototype
上的 toString
方法。当对象本身没有 toString
方法时,它会沿着原型链向上查找并调用继承的 toString
方法。
原型链的概念使得 JavaScript 中的对象可以实现继承和共享属性与方法的特性。通过在构造函数的原型上定义属性和方法,可以使所有通过该构造函数创建的对象共享这些属性和方法。同时,原型链也提供了一种方式,让对象在没有某个属性或方法时,能够查找并使用它们。
2.2.3 ES6 中的类和继承
在 ECMAScript 6(ES6)中引入了类(class)和继承的语法糖,使得在 JavaScript 中实现面向对象编程更加直观和易用。
类(Class)是一种用于创建对象的模板或蓝图,它定义了对象的属性和方法。可以通过关键字 class
声明一个类,并使用 constructor
方法定义构造函数。
以下是一个示例展示如何使用类创建对象:
class Person {constructor(name) {this.name = name;}greet() {console.log(`Hello, my name is ${this.name}.`);}
}const person = new Person("John");
person.greet(); // 输出:Hello, my name is John.
在上面的例子中,Person
类定义了一个构造函数 constructor
和一个方法 greet
。通过 new
关键字可以创建 Person
类的对象实例,并调用该对象的方法。
继承(Inheritance)允许一个类继承另一个类的属性和方法,并且可以在此基础上进行扩展。在 ES6 中,可以使用关键字 extends
来实现类的继承。
以下是一个示例展示如何在 ES6 中实现类的继承:
class Animal {constructor(name) {this.name = name;}eat() {console.log(`${this.name} is eating.`);}
}class Dog extends Animal {bark() {console.log(`${this.name} is barking.`);}
}const dog = new Dog("Max");
dog.eat(); // 输出:Max is eating.
dog.bark(); // 输出:Max is barking.
在上面的例子中,Dog
类继承了 Animal
类,通过 extends
关键字指定了父类。子类可以访问父类的属性和方法,并可以在子类中定义自己的属性和方法。
类和继承提供了一种更直观、更清晰的方式来实现面向对象编程,使得代码更易读、易扩展和易维护。它们在 JavaScript 中成为了一种常见的编程范式,尤其在构建大型应用程序时非常有用。