🤔️《你不知道的JavaScript》到底讲了些什么?
开始之前
在计算机科学的领域中,JavaScript是一门无法忽视的重要语言,深受许多开发者的喜爱。然而,它背后隐藏的复杂性和奥秘许多开发者并不为知。《你不知道的JavaScript》这三卷之作,对我个人而言真的算是常看常新,从刚从事前端开发到如今已经独当一面,从这本书中受益良多。因此在多次阅读后我选择用内容梗概
+案例解析
的形式将其精华部分记录下来,以供个人翻阅和与大家分享,那么我们开始吧
上卷
上卷主要针对语言核心的一些关键概念,如作用域、闭包、this等。本文将为笔者阅读过程中所总结和提炼的关键知识点与经典案例
1. 作用域是什么?
内容概览
本章介绍了JavaScript中的作用域概念,解释了变量如何被储存以及如何被引用。
实例分析
var a = 2;
function foo() {
var a = 3;
console.log(a); // 3
}
foo();
console.log(a); // 2
在这个例子中,我们看到a
在全局作用域和foo
函数的作用域中都有定义。函数内部的a
不会影响到全局作用域中的a
。
2. 词法作用域
内容概览
词法作用域意味着作用域是由函数声明的位置来决定的,而不是函数调用的位置。
实例分析
function foo() {
console.log(a);
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar(); // 2
尽管foo
函数在bar
函数内部被调用,但foo
函数的词法作用域仍然使其能够访问外部的变量a
,所以输出为2。
3. 函数与块作用域
内容概览
介绍了函数作用域和块作用域,以及如何利用它们来避免变量冲突和其他问题。
实例分析
if (true) {
let a = 2;
console.log(a); // 2
}
console.log(a); // ReferenceError
使用let
定义的变量具有块作用域,只能在声明它的块中访问。
4. 提升
内容概览
解释了提升(hoisting)现象,即变量和函数声明会被移动到它们所在的作用域顶部。
实例分析
foo(); // "Hello"
function foo() {
console.log("Hello");
}
尽管函数foo
在调用之后被声明,但由于提升,它仍然可以正常调用。
5. 作用域闭包
内容概览
解释了闭包是如何工作的,以及它在JavaScript中的重要性。
实例分析
function makeGreeting(greeting) {
return function(name) {
console.log(greeting + ", " + name);
};
}
let sayHello = makeGreeting("Hello");
sayHello("Alice"); // "Hello, Alice"
sayHello
函数是一个闭包,它记住了创建它时的作用域,因此能够访问greeting
变量。
6. 词法分析和语法分析
实例分析
来看以下代码:
function add(x, y) {
return x + y;
}
let sum = add(5, 7);
在词法分析阶段,这段代码可能被分解为多个词法单元:function
, add
, (
, x
, ,
, y
, )
, {
, return
, +
, ;
, }
, let
, =
, 5
, 7
等。然后,语法分析器会将这些词法单元组合成AST。
7. L查询与R查询
实例分析
function calculateArea(radius) {
const pi = 3.141592653589793;
return pi * radius * radius;
}
let r = 5;
let area = calculateArea(r);
在这个例子中,考虑let area = calculateArea(r);
这行代码。对于calculateArea
,它是RHS查询,因为我们需要获得这个函数的引用来执行它。而r
也是RHS查询,因为我们正在获取它的值来传递给函数。
在calculateArea
函数内,pi
和两次radius
的查询都是RHS查询,因为我们获取它们的值来执行乘法操作。而return
语句中的计算结果则赋值给了隐式的返回值,这涉及到LHS查询。
对于let r = 5;
,这里的r
是一个LHS查询,因为我们给它赋值了。
中卷
中卷的内容相比上卷来说更加深入且晦涩,其中包括令初学者头昏脑胀的面向对象编程与this原型链相关的知识,我将以更多的篇幅和更深入的案例来帮助大家进行理解
1. 对象
实例分析 1
使用工厂函数和构造器来创建对象:
function createPerson(name, age) {
return {
name,
age,
greet() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
};
}
const person1 = createPerson('Alice', 30);
person1.greet();
深入分析
这是一个工厂函数的例子,允许我们快速创建具有相似属性和方法的对象。在此,greet
方法是每个对象的一部分,这可能导致内存浪费,因为每次创建新对象时,都会为greet
方法分配新的内存。
实例分析 2
使用getters和setters:
const book = {
title: 'In Search of Lost Time',
author: 'Marcel Proust',
get description() {
return `${this.title} by ${this.author}`;
},
set description(value) {
[this.title, this.author] = value.split(' by ');
}
};
book.description = '1984 by George Orwell';
console.log(book.title); // Outputs: 1984
深入分析
这个案例展示了如何利用对象的getters和setters来动态地管理对象的属性。通过setter,我们能够同时更新title
和author
,而getter则为我们提供了书的描述。
2. 类
实例分析 1
多态的使用:
class Animal {
makeSound() {
console.log('Some generic sound');
}
}
class Dog extends Animal {
makeSound() {
console.log('Woof');
}
}
const animal1 = new Animal();
const animal2 = new Dog();
animal1.makeSound(); // Outputs: Some generic sound
animal2.makeSound(); // Outputs: Woof
深入分析
多态是面向对象编程中的一个关键概念,允许我们创建能够以多种形式表现的对象。在此,我们看到Dog
类重写了Animal
类的makeSound
方法,实现了多态。
实例分析 2
静态方法的使用:
class MathUtility {
static add(x, y) {
return x + y;
}
}
console.log(MathUtility.add(5, 3)); // Outputs: 8
深入分析
这个案例展示了如何在类中使用静态方法。与实例方法不同,静态方法不需要创建类的实例就可以被调用。它们通常用于执行与类的实例无关的操作。
3. 原型
实例分析
一个动态添加到原型的方法:
function Cat(name) {
this.name = name;
}
Cat.prototype.purr = function() {
console.log(`${this.name} is purring.`);
};
const whiskers = new Cat('Whiskers');
whiskers.purr(); // Outputs: Whiskers is purring.
深入分析
在此例中,我们后期将purr
方法添加到Cat
的原型中。这意味着即使在添加此方法后创建的所有Cat
实例都可以访问它。这展示了原型继承的动态性质:我们可以在任何时候修改原型,这些更改会反映在所有继承了那个原型的对象上。
4. this和对象原型
JavaScript中的this
是一个非常深入且经常被误解的主题。this
并不是由开发者选择的,它是由函数调用时的条件决定的。
实例分析
考虑以下场景:
function showDetails() {
console.log(this.name);
}
const obj1 = {
name: 'Object 1',
display: showDetails
};
const obj2 = {
name: 'Object 2',
display: showDetails
};
obj1.display(); // Outputs: Object 1
obj2.display(); // Outputs: Object 2
深入分析
在这里,showDetails
函数查看this.name
。当它作为obj1
的方法被调用时,this
指向obj1
。当它作为obj2
的方法被调用时,this
指向obj2
。这说明了this
的动态性质:它是基于函数如何被调用的。
5. 原型链
当试图访问一个对象的属性或方法时,JavaScript会首先在该对象本身上查找。如果未找到,它会在对象的原型上查找,然后是原型的原型,以此类推,直到找到该属性或到达原型链的末尾。
实例分析
function Animal(sound) {
this.sound = sound;
}
Animal.prototype.makeSound = function() {
console.log(this.sound);
}
function Dog() {
Animal.call(this, 'Woof');
}
Dog.prototype = Object.create(Animal.prototype);
const dog = new Dog();
dog.makeSound(); // Outputs: Woof
深入分析
当我们调用dog.makeSound()
时,JavaScript首先在dog
对象上查找makeSound
。未找到后,它会在Dog
的原型上查找。还是未找到,然后继续在Animal
的原型上查找,最后找到并执行它。
6. 行为委托
行为委托是原型的一种使用模式,涉及到对象之间的关系,而不仅仅是克隆或复制。
实例分析
const Task = {
setID: function(ID) { this.id = ID; },
outputID: function() { console.log(this.id); }
};
const XYZ = Object.create(Task);
XYZ.prepareTask = function(ID, Label) {
this.setID(ID);
this.label = Label;
};
XYZ.outputTaskDetails = function() {
this.outputID();
console.log(this.label);
};
const task = Object.create(XYZ);
task.prepareTask(1, 'create demo for delegation');
task.outputTaskDetails(); // Outputs: 1, create demo for delegation
深入分析
XYZ
不是Task
的复制,它链接到Task
。当我们在XYZ
对象上调用setID
或outputID
方法时,这些方法实际上是在Task
对象上运行的,但this
指向的是XYZ
。这就是所谓的委托:XYZ
在行为上委托给了Task
。
下卷
下卷的内容相较于中卷就基础了很多,更偏向于实际应用方向
1. 类型和语法
实例分析 - 类型转换
考虑以下的隐式类型转换:
var a = "42";
var b = a * 1;
console.log(typeof a); // "string"
console.log(typeof b); // "number"
深入分析
在这里,变量a
是一个字符串,但当我们尝试与数字进行乘法操作时,它会被隐式地转换为一个数字。这是因为乘法操作符期望它的操作数是数字,因此JavaScript会尝试将字符串a
转换为一个数字。
2. 异步和性能
实例分析 - Promises
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data fetched!");
}, 2000);
});
}
fetchData().then(data => {
console.log(data); // Outputs: "Data fetched!" after 2 seconds
});
深入分析
Promises 提供了一种更简洁、更具可读性的方式来处理异步操作。在上面的例子中,fetchData
函数返回一个Promise。setTimeout
模拟了异步数据获取,数据在2秒后可用。当数据准备好后,resolve
函数被调用,then
方法随后执行,输出数据。
3. ES6及其以上的特性
实例分析 - 使用箭头函数
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8]
深入分析
箭头函数提供了一种更简洁的方式来定义函数,尤其是对于那些简短的、无状态的函数来说。在上述例子中,我们使用箭头函数简洁地定义了一个函数,该函数将其输入值乘以2,并使用map
方法将其应用到一个数字数组中。
实例分析 - 使用async/await
async function fetchDataAsync() {
let response = await fetch('https://api.example.com/data');
let data = await response.json();
return data;
}
fetchDataAsync().then(data => console.log(data));
深入分析
async
/await
是ES7引入的特性,允许以同步的方式编写异步代码。在这个案例中,fetchDataAsync
函数是一个异步函数,这意味着它返回一个Promise。await
关键字使我们能够等待Promise解析,然后继续执行后面的代码。这消除了回调地狱,使异步代码更容易阅读和维护。
4. 迭代器和生成器
实例分析 - 使用生成器函数
function* numbersGenerator() {
yield 1;
yield 2;
yield 3;
}
const numbers = numbersGenerator();
console.log(numbers.next().value); // 1
console.log(numbers.next().value); // 2
console.log(numbers.next().value); // 3
深入分析
生成器函数使用function*
声明,并且可以包含一个或多个yield
表达式。每次调用生成器对象的next()
方法时,函数都会执行到下一个yield
表达式,并返回其值。这使我们能够按需产生值,非常适用于大数据集或无限数据流。
5. 增强的对象字面量
实例分析
const name = "Book";
const price = 20;
const book = {
name,
price,
describe() {
return `${this.name} costs ${this.price} dollars.`;
}
};
console.log(book.describe()); // "Book costs 20 dollars."
深入分析
增强的对象字面量允许我们在声明对象时使用更简洁的语法。在这里,我们直接使用变量名作为键,并使用简短的方法定义形式。这使得对象声明更为简洁和可读。
6. 解构赋值
实例分析
const user = {
firstName: "Alice",
lastName: "Smith"
};
const { firstName, lastName } = user;
console.log(firstName); // Alice
console.log(lastName); // Smith
深入分析
解构赋值允许我们从数组或对象中提取数据,并赋值给新的或已存在的变量。在此例中,我们从user
对象中提取了firstName
和lastName
属性,并将它们赋值给了同名的新变量。
7. 模块
实例分析 - ES6模块导入和导出
// math.js
export function add(x, y) {
return x + y;
}
export function subtract(x, y) {
return x - y;
}
// app.js
import { add, subtract } from './math.js';
console.log(add(5, 3)); // 8
console.log(subtract(5, 3)); // 2
结语
经过对《你不知道的JavaScript》上、中、下三卷的深入探索,我们更加清晰地理解了JavaScript这门语言的复杂性、深度和强大之处。这不仅仅是关于语法或是新特性,更是关于理解其背后的哲学和设计思想。作为开发者,真正的掌握并不只是会用,而是要知其所以然。此书为我们打开了一扇探索JavaScript的大门,但真正的旅程,才刚刚开始。我们的每一步前行,都是为了更好地理解、更精准地应用,为编写出更高效、更优雅的代码而努力。
链接:https://juejin.cn/post/7274839607656890429
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。