注册

🤔️《你不知道的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,我们能够同时更新titleauthor,而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对象上调用setIDoutputID方法时,这些方法实际上是在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对象中提取了firstNamelastName属性,并将它们赋值给了同名的新变量。


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的大门,但真正的旅程,才刚刚开始。我们的每一步前行,都是为了更好地理解、更精准地应用,为编写出更高效、更优雅的代码而努力。


作者:J3LLYPUNK
链接:https://juejin.cn/post/7274839607656890429
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册