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 中,函数式编程可以通过以下几个方面来实现:
function square(x) {
return x * x;
}
const result = square(5); // 纯函数调用
const numbers = [1, 2, 3, 4, 5];
const doubledNumbers = numbers.map((num) => num * 2); // 创建新的数组副本
function multiplyBy(factor) {
return function (x) {
return x * factor;
};
}
const triple = multiplyBy(3); // 返回一个函数
const result = triple(5); // 调用返回的函数
compose
或pipe
)将多个函数按特定顺序组合起来,形成一个新的函数。const add = (x) => x + 1;
const multiply = (x) => x * 2;
const combined = compose(multiply, add); // 组合函数
const result = combined(3); // 调用组合后的函数
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 实现继承的机制之一。
__proto__
属性访问(不推荐使用)或使用 Object.getPrototypeOf(obj)
方法获取。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 中成为了一种常见的编程范式,尤其在构建大型应用程序时非常有用。