🤔️《你不知道的JavaScript》到底讲了些什么?

开始之前

在计算机科学的领域中,JavaScript是一门无法忽视的重要语言,深受许多开发者的喜爱。然而,它背后隐藏的复杂性和奥秘许多开发者并不为知。《你不知道的JavaScript》这三卷之作,对我个人而言真的算是常看常新,从刚从事前端开发到如今已经独当一面,从这本书中受益良多。因此在多次阅读后我选择用内容梗概+案例解析的形式将其精华部分记录下来,以供个人翻阅和与大家分享,那么我们开始吧

上卷

上卷主要针对语言核心的一些关键概念,如作用域、闭包、this等。本文将为笔者阅读过程中所总结和提炼的关键知识点与经典案例

1. 作用域是什么?

内容概览

本章介绍了JavaScript中的作用域概念,解释了变量如何被储存以及如何被引用。

实例分析

js 复制代码
var a = 2;

function foo() {
    var a = 3;
    console.log(a); // 3
}

foo();

console.log(a); // 2

在这个例子中,我们看到a在全局作用域和foo函数的作用域中都有定义。函数内部的a不会影响到全局作用域中的a

2. 词法作用域

内容概览

词法作用域意味着作用域是由函数声明的位置来决定的,而不是函数调用的位置。

实例分析

js 复制代码
function foo() {
    console.log(a);
}

function bar() {
    var a = 3;
    foo();
}

var a = 2;

bar(); // 2

尽管foo函数在bar函数内部被调用,但foo函数的词法作用域仍然使其能够访问外部的变量a,所以输出为2。

3. 函数与块作用域

内容概览

介绍了函数作用域和块作用域,以及如何利用它们来避免变量冲突和其他问题。

实例分析

js 复制代码
if (true) {
    let a = 2;
    console.log(a); // 2
}

console.log(a); // ReferenceError

使用let定义的变量具有块作用域,只能在声明它的块中访问。

4. 提升

内容概览

解释了提升(hoisting)现象,即变量和函数声明会被移动到它们所在的作用域顶部。

实例分析

js 复制代码
foo(); // "Hello"

function foo() {
    console.log("Hello");
}

尽管函数foo在调用之后被声明,但由于提升,它仍然可以正常调用。

5. 作用域闭包

内容概览

解释了闭包是如何工作的,以及它在JavaScript中的重要性。

实例分析

js 复制代码
function makeGreeting(greeting) {
    return function(name) {
        console.log(greeting + ", " + name);
    };
}

let sayHello = makeGreeting("Hello");
sayHello("Alice"); // "Hello, Alice"

sayHello函数是一个闭包,它记住了创建它时的作用域,因此能够访问greeting变量。

6. 词法分析和语法分析

实例分析

来看以下代码:

js 复制代码
function add(x, y) {
    return x + y;
}

let sum = add(5, 7);

在词法分析阶段,这段代码可能被分解为多个词法单元:function, add, (, x, ,, y, ), {, return, +, ;, }, let, =, 5, 7 等。然后,语法分析器会将这些词法单元组合成AST。

7. L查询与R查询

实例分析

js 复制代码
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

使用工厂函数和构造器来创建对象:

js 复制代码
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:

js 复制代码
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

多态的使用:

js 复制代码
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

静态方法的使用:

js 复制代码
class MathUtility {
    static add(x, y) {
        return x + y;
    }
}

console.log(MathUtility.add(5, 3));  // Outputs: 8

深入分析

这个案例展示了如何在类中使用静态方法。与实例方法不同,静态方法不需要创建类的实例就可以被调用。它们通常用于执行与类的实例无关的操作。

3. 原型

实例分析

一个动态添加到原型的方法:

js 复制代码
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并不是由开发者选择的,它是由函数调用时的条件决定的。

实例分析

考虑以下场景:

js 复制代码
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会首先在该对象本身上查找。如果未找到,它会在对象的原型上查找,然后是原型的原型,以此类推,直到找到该属性或到达原型链的末尾。

实例分析

js 复制代码
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. 行为委托

行为委托是原型的一种使用模式,涉及到对象之间的关系,而不仅仅是克隆或复制。

实例分析

js 复制代码
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. 类型和语法

实例分析 - 类型转换

考虑以下的隐式类型转换:

js 复制代码
var a = "42";
var b = a * 1;
console.log(typeof a);  // "string"
console.log(typeof b);  // "number"

深入分析

在这里,变量a是一个字符串,但当我们尝试与数字进行乘法操作时,它会被隐式地转换为一个数字。这是因为乘法操作符期望它的操作数是数字,因此JavaScript会尝试将字符串a转换为一个数字。

2. 异步和性能

实例分析 - Promises

js 复制代码
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及其以上的特性

实例分析 - 使用箭头函数

js 复制代码
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(num => num * 2);
console.log(doubled);  // [2, 4, 6, 8]

深入分析

箭头函数提供了一种更简洁的方式来定义函数,尤其是对于那些简短的、无状态的函数来说。在上述例子中,我们使用箭头函数简洁地定义了一个函数,该函数将其输入值乘以2,并使用map方法将其应用到一个数字数组中。

实例分析 - 使用async/await

js 复制代码
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. 迭代器和生成器

实例分析 - 使用生成器函数

js 复制代码
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. 增强的对象字面量

实例分析

js 复制代码
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. 解构赋值

实例分析

js 复制代码
const user = {
    firstName: "Alice",
    lastName: "Smith"
};

const { firstName, lastName } = user;

console.log(firstName);  // Alice
console.log(lastName);   // Smith

深入分析

解构赋值允许我们从数组或对象中提取数据,并赋值给新的或已存在的变量。在此例中,我们从user对象中提取了firstNamelastName属性,并将它们赋值给了同名的新变量。

7. 模块

实例分析 - ES6模块导入和导出

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

相关推荐
New.file2 分钟前
AJAX详解
前端·ajax·okhttp
小七蒙恩30 分钟前
java 上传txt json等类型文件解析后返回给前端
java·前端·json
糕冷小美n1 小时前
jeecgbootvue3列表数据状态为数字时,手动赋值的三种方法
前端·javascript·vue.js
mqiqe1 小时前
Nginx 配置前端后端服务
运维·前端·nginx
小羊小羊,遇事不难2 小时前
Error: near “112136084“: syntax
java·服务器·前端
Domain-zhuo3 小时前
CSS实现一个自定义的滚动条
前端·javascript·css·vue.js·git·node.js
autumn8683 小时前
css的长度单位有那些?
前端·css
李贺梖梖3 小时前
CSS2笔记
前端
张丹 新叶之扉3 小时前
vue的整理
前端·javascript·vue.js
鱼大大博客3 小时前
选择Edge Scdn时应考虑哪些因素?
前端·edge·ddos