JavaScript 基础

数据、运算与判断

隐式转换

当运算符两端数据类型不同时,如 '1' + 2 ,若没有进行显式类型转换,则 JavaScript 解释器将对其做隐式类型转换。JavaScripit 的隐式类型转换常常让初学者感到疑惑,这是因为 JavaScript 不同基本数据类型之间没有明确的优先级关系,不存在所谓的“低级转向高级”,也不存在类似 C/C++ 的重载运算符操作;此外,优先级相同的运算符如 +- 可导致截然不同的隐式转换逻辑。

post-udemy-javascript-js-vs-christianity

即便如此,JavaScript 的的隐式转换规则仍然有迹可循。

  • + 运算可将两侧变量隐式转换为 String 类型:

    1
    "12" + 34; //  ->  '1234'
  • - 运算可将两侧变量隐式转换为 Number 类型:

    1
    "12" - 34; //  ->  -22
  • <=, <, >=, > 可将两侧变量隐式转换为 Number 类型:

  • 同级运算与隐式转换从左到右顺次进行:

    1
    5 + 6 + "4" + 9 - 4 - 2; //  ->  1143

函数与对象

函数的三种形式

JavaScript 有三种主要的函数表示形式,假设我们要定义一个函数,传入一个参数 population ,返回传入人口参数占世界的百分比,三种形式如下所示:

  • 声明形式(Declaration)

    1
    2
    3
    4
    function percentageOfWorld1(population) {
    const totalPopulationOfWorld = 7900;
    return `${(population / totalPopulationOfWorld) * 100}%`;
    }
  • 匿名形式(Expressions)

    1
    2
    3
    4
    const percentageOfWorld2 = function (population) {
    const totalPopulationOfWorld = 7900;
    return `${(population / totalPopulationOfWorld) * 100}%`;
    };
  • 箭头函数(Arrow Function)

    1
    2
    3
    4
    const percentageOfWorld3 = (population) => {
    const totalPopulationOfWorld = 7900;
    return `${(population / totalPopulationOfWorld) * 100}%`;
    };

设全球总人口为 7900M ,中国人口为 1441M ,调用结果如下所示:

1
2
3
percentageOfWorld1(1441); //  ->  18.240506329113924%
percentageOfWorld2(1441); // -> 18.240506329113924%
percentageOfWorld3(1441); // -> 18.240506329113924%

箭头函数的不适用场景

  • 函数作为对象属性时,应该使用匿名函数,而不是箭头函数,如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    let myCountry = {
    country: "China",
    capital: "Beijing",
    language: "Chinese",
    population: 1300,
    neighbours: ["Japan", "Russia", "Korea", "India"],
    describe: function () {
    console.log(
    `${this.country} has ${this.population} million ${this.language}-speaking people, ${this.neighbours.length} neighbouring countries and a capital called ${this.capital}`
    );
    },

    // 不应该使用箭头函数
    // describe: () => {
    // console.log(
    // `${this.country} has ${this.population} million ${this.language}-speaking people, ${this.neighbours.length} neighbouring countries and a capital called ${this.capital}.`
    // );
    // },
    };

    箭头函数作为对象属性时, this 并非指向对象本身,原型链失效,从而使得用 this 访问对象属性将导致意外情况。

数组与循环

DOM 树与事件处理

HTML 文件在浏览器内被解析为 DOM 树。

获取 HTML 元素

浏览器的 Web API 提供了 document 对象,用于对 DOM 树进行操作。

  • document.querySelector() 可通过传入 CSS 选择器,选择符合要求的第一个元素节点,返回对该元素对象的引用:

    1
    2
    const body = document.querySelector("body"); //  选择 body 元素,
    const guess = document.querySelector(".guess"); // 选择第一个 class="guess" 的元素
  • document.querySelectorAll() 返回符合选择器的所有元素节点,即一个元素对象数组。

监听点击事件

  • element.addEventListener()element 元素对象注册一个监听器,当元素符合监听行为时,立刻执行传入的函数:

    1
    2
    3
    4
    // 为 check 元素注册点击事件的监听器
    check.addEventListener("click", () => {
    statements...
    });

    注意到传入的第二个参数是一个函数,这里体现了 JavaScript 中函数和其他对象(object)地位等同,函数的内容就是其本身 object 的值。

编辑元素 CSS 样式

  • element.style.property 返回对元素某个样式的引用:

    1
    2
    const message = document.querySelector(".message");
    message.style.fontSize = "2rem";

    注意到 CSS 中字体大小属性为 font-size ,但是这在 JavaScript 中不是合法的变量名,需改为驼峰命名法的 fontSize

处理元素的 class

  • element.classList 返回元素的 class 列表,可使用 .add().remove() 对其进行增删;使用 .toggle() 可实现在没有该类时添加该类,有该类时移除该类;.contains() 判断是否存在某个类:

    1
    2
    3
    4
    5
    const modal = document.querySelector(".modal");
    modal.classList.add("hidden"); //添加 hidden 类
    modal.classList.remove("hidden"); //移除 hidden 类
    modal.classList.toggle("hidden"); //检查是否存在 hidden 类,若不存在则添加,否则移除
    modal.classList.contains("hidden"); //检查是否存在 hidden 类
  • element.className.includes("class")" 同样返回是否存在该类。

    1
    2
    const modal = document.querySelector(".modal");
    modal.className.includes("hidden"); //检查是否存在 hidden 类

JavaScript 幕后解析

总览

现代 JavaScript 语言拥有如下特性

  • 是一门高级语言
  • 支持内存回收
  • 解释型或 JIT 编译型语言
  • 支持多种编程范式:面向过程、面向对象、函数式
  • 基于原型与面向对象
  • 函数与变量同级
  • 动态类型
  • 单线程
  • 非阻塞性事件循环

JavaScript 引擎与运行环境

  • JavaScript 引擎主要由两个部分组成:调用栈和堆。

    调用栈主要用于存储函数执行的上下文,堆用于存储引用指向的数据实例。

    note-javascript-engine

  • ”JavaScript 是一门解释型“语言可能是 JavaScript 普遍存在的最大认知误区之一。初出茅庐的 JavaScript 确实如此,但现代浏览器或形如 Node.js 的运行环境使用的是 JIT(Just-in-time)方法。

    note-javascript-jit

    在 JIT 流程中,JavaScript 代码会先被转换成抽象语法树(AST),使用解释器执行,而 Baselines compiler 与 Optimization compiler 会在后台评估每行代码的执行命中率,逐渐执行与传统编译型语言类似的工作,提高运行效率。

  • 浏览器端的 JavaScript 的运行环境由 JavaScript 引擎,浏览器提供的 Web API ,回调队列构成。JavaScript 引擎和回调队列组成非阻塞性事件循环。 Node.js 中的运行环境则是移除了 Web API ,并添加了一些 Native 层的内容。

    note-javascript-browser-runtime

执行上下文

JavaScript 的执行上下文是一个很抽象的概念,其相当于代码执行的一个“环境”,存储了代码执行所需的一些信息,通常包括:

  • 环境中的变量
  • 作用域链
  • this 关键字对象

这好比享用外卖 Pizza 时,我们不仅需要从包装盒里取出 Pizza 本身,还需要拿出一次性餐具,必要时取出收据进行检查。包装盒就是“上下文”,Pizza 是我们要执行的对象,餐具和收据是环境中的变量。

note-javascript-execution-context

运行在 JavaScript 最外层的代码所处的是全局上下文(Global execution context),在编译完成后被创建,并执行其中的代码,之后等待事件循环的回调函数。

此外,每个函数的执行都会创建自己的上下文。

调用栈、作用域与作用域链

函数的每一次执行都会创建自己的上下文,并压入调用栈中,执行完后弹出栈顶,并回到调用这个函数的地方。这种设计可以防止上下文丢失。

作用域指的是变量可被获得(access)的区域(scope),JavaScript 的作用域分为全局作用域、函数作用域、块级作用域。内部作用域可以访问外部作用域的变量,反之未然 ,这便是作用域链。

var 变量是函数级作用域的,letconst 均是块级。

作用域链和调用栈没有任何关系。

note-javascript-call-stack-and-scope-chain

声明提升与临时静默域(TDZ)

函数的声明提升,即运行调用写在声明之前,是高级语言解决函数相互引用问题的常见特性。而 var 类型也变量允许声明则是 JavaScript 早期为支持 first-class function 所产生的历史遗留问题。

var 变量在被声明前引用的值是 undefined ,表面上看是 JavaScript 将声明提前了,其实不然。实际上 JavaScript 在每个执行上下文之前都会扫描整个作用域,检查那些变量需要做声明提升。

是否声明提升 初值 作用域
函数变量 函数值本身 块级
var 变量 undefined 函数级
letconst 变量 <uninitialized>,TDZ 块级
函数表达式和箭头函数 取决于是 varlet 还是 const

note-javascript-tdz

var 类型变量、包括函数在内当前上下文内的对象都是父对象的成员变量

this 关键字

this 是每个上下文创建的特殊变量,最常见的用法是,对象的成员函数作用域内的 this 指向对象本身。

其他情况下 this 的指向如表所示。

类型 指向
方法 调用之的对象
单独调用函数 undefined(严格模式下)
箭头函数 外部作用域的 this
监听器 监听到的 DOM 元素

正则函数与箭头函数

声明式函数与匿名函数被称为正则函数(regular function),其与箭头函数的区别主要在 thisarguments 变量上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const vonbrank = {
year: 2001,
calcAge: function () {
console.log(this);
console.log(this.year);

const foo1 = function () {
console.log(this.year);
// this == undefined,会报 reference error
};
foo1();

const self = this;
const foo1 = function () {
this = self;
console.log(this.year);
// 将外部的 this 保存为 self 传进来,可以正常运行
};
foo2();


const foo3 = () => {
console.log(this);
console.log(this.year);
// 箭头函数本身不不包含 this 对象,沿着作用域链网上找能找到 calcAge 的 this ,即 vonbrank
};

foo3();
},
greeting: () => console.log(this),
//同样的 greeting 内没有 this 参数,沿作用域链向上找到全局作用域,而全局作用域也没有 this 对象,所以输出 undefined
};
vonbrank.calcAge();
vonbrank.greeting();

正则函数拥有 arguments 变量,这是传入参数构成的数组。箭头函数则没有。

1
2
3
4
5
6
7
8
const addExpr = function (a, b) {
console.log(arguments);
};

const addArrow = (a, b) => console.log(arguments);

addExpr(2, 5); //[2, 5]
addArrow(2, 5); //reference error

原始类型与对象引用

JavaScript 中的原始类型(primitive types)有:

  • Number
  • String
  • Boolean
  • Undefined
  • Null
  • Symbol
  • BigInt

对象类型(reference types)有:

  • Object 字面量
  • 数组
  • 函数

等。

原始类型和对象引用的名称都存储在 JavaScript 引擎的 Identifier 中。

note-javascript-primitive-objects

原始类型的数据值存储在调用栈(CALL STACK)的上下文中,每次修改时,由于调用栈中的数据一旦创建便不可修改。因此会在调用栈中开辟新的内存,存储修改后的值,同时 Identifier 令名称指向新的内存。这便解释了如下代码。

1
2
3
4
5
6
const age = 30;
const oldAge = age;
age = 27;

console.log(age); //27
console.log(oldAge); //30

数据引用会将数据实际存储在堆区(HEAP),栈区存储指向堆区实例的地址(引用),多个变量名指向同一个引用,修改时也会改变堆区同一个地址的内容。

1
2
3
4
5
6
7
8
9
10
const me = {
name: "Jonas",
age: 30,
};

const friend = me;
friend.age = 27;

console.log(friend.age); //27
console.log(me.age); //27

如果确实要创建两个值相同,引用不同的对象,可以使用 Object.assign()

1
2
3
const source = { b: 4, c: 5 };
const newSource = Object.assign({}, source);
console.log(newSource); //{ b: 4, c: 5 }

但这样只是浅度拷贝,即如果原对象有对象成员,拷贝结果仍然是该成员的引用。实现引用对象的深度拷贝很多时候是一件比较困难的事情,原生 JavaScript 并没有提供深度拷贝方法,可以自己手动实现,或调用外部库。

JavaScript 高级编程

为方便模拟实际项目的情况,本节默认全局上下文已执行以下语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
"use strict";

// Data needed for a later exercise
const flights =
"_Delayed_Departure;fao93766109;txl2133758440;11:25+_Arrival;bru0943384722;fao93766109;11:45+_Delayed_Arrival;hel7439299980;fao93766109;12:05+_Departure;fao93766109;lis2323639855;12:30";

// Data needed for first part of the section
const restaurant = {
name: "Classico Italiano",
location: "Via Angelo Tavanti 23, Firenze, Italy",
categories: ["Italian", "Pizzeria", "Vegetarian", "Organic"],
starterMenu: ["Focaccia", "Bruschetta", "Garlic Bread", "Caprese Salad"],
mainMenu: ["Pizza", "Pasta", "Risotto"],

openingHours: {
thu: {
open: 12,
close: 22,
},
fri: {
open: 11,
close: 23,
},
sat: {
open: 0, // Open 24 hours
close: 24,
},
},
};

解构赋值

  • 数组解构赋值

    数组解构赋值可以实现将数组批量赋值给一组变量。

    1
    2
    3
    const arr = [2, 3, 4];
    const [x, y, z] = arr;
    console.log(x, y, z); //2 3 4

    还可以跳过某些元素。

    1
    2
    3
    const arr = [2, 3, 4];
    const [x, , y] = arr;
    console.log(x, y); //2 4

    用于快速交换变量的值。

    1
    2
    3
    4
    let x = 1,
    y = 2;
    [x, y] = [y, x];
    console.log(x, y); //2 1

    接受函数返回多个变量。

    1
    2
    3
    4
    5
    const order = function () {
    return [2, 5];
    };
    let [x, y] = order();
    console.log(x, y); //2 5

    嵌套解构赋值。

    1
    2
    3
    const nest = [2, 4, [5, 6]];
    const [i, , [j, k]] = nest;
    console.log(i, j, k);

    默认解构赋值。

    1
    2
    3
    4
    const [p = 1, q = 1, r] = [8, 9];
    console.log(p, q, r); //8 9 undefined
    [p = 1, q = 1, r = 1] = [8, 9];
    console.log(p, q, r); //8 9 1
  • 对象解构赋值

    对象解构赋值和数组类似,括号变成花括号。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const { name, openingHours, categories } = restaurant;
    console.log(name, openingHours, categories);
    /*
    Classico
    Italiano {
    thu: { open: 12, close: 22 },
    fri: { open: 11, close: 23 },
    sat: { open: 0, close: 24 }
    }
    [ 'Italian', 'Pizzeria', 'Vegetarian', 'Organic' ]
    */

    使用不同的变量名。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const {
    name: restaurantName,
    openingHours: hours,
    categories: tags,
    } = restaurant;
    console.log(restaurantName, hours, tags);
    /*
    Classico
    Italiano {
    thu: { open: 12, close: 22 },
    fri: { open: 11, close: 23 },
    sat: { open: 0, close: 24 }
    }
    [ 'Italian', 'Pizzeria', 'Vegetarian', 'Organic' ]
    */

    设置默认值。

    1
    2
    const { menu = [], starterMenu: starters = [] } = restaurant;
    console.log(menu, starters); //[] [ 'Focaccia', 'Bruschetta', 'Garlic Bread', 'Caprese Salad' ]

    多个变量重新赋值,注意,由于 JavaScript 会将大括号开头的行视作一个新的作用域,因此需要用括号括起来防止歧义。

    1
    2
    3
    4
    5
    6
    let a = 111;
    let b = 999;
    const obj = { a: 23, b: 7, c: 14 };

    ({ a, b } = obj);
    console.log(a, b);

    子对象解构赋值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const { openingHours } = restaurant;
    const {
    fri: { open, close },
    } = openingHours;
    console.log(open, close); // 11 23

    //依然可以使用类似方法重命名,节省大量代码
    const {
    fri: { open: o, close: c },
    } = openingHours;
    console.log(o, c); // 11 23

    作为函数参数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    restaurant.orderDelivery = function ({
    starterIndex = 1,
    mainIndex = 0,
    time = "20:00",
    address,
    }) {
    console.log(starterIndex, mainIndex, time, address);
    };
    restaurant.orderDelivery({
    time: "22:30",
    address: "Via del sole, 21",
    mainIndex: 2,
    starterIndex: 2,
    }); //2 2 22:30 Via del sole, 21

扩张运算符 ...rest 参数运算符

JavaScript 中的 ... 运算符用于将可迭代对象(包括数组,字符串,mapset 等)分解为一组单个元素,这些元素是原对象的浅层拷贝。

1
2
3
4
const name = "vonbrank";
const letters = [...name, " ", ".s"];
console.log(letters);
//['v', 'o', 'n', 'b', 'r', 'a', 'n', 'k', ' ', '.s']

合并数组。

1
2
3
4
const arr = [7, 8, 9];
const newArr = [1, 2, ...arr];
console.log(newArr); //[1, 2, 7, 8, 9]
console.log(...arr); //[7, 8, 9]

作为函数参数传递。

1
2
3
4
5
6
restaurant.orderPasta = function (ing1, ing2, ing3) {
console.log(ing1, ing2, ing3);
};

const ingredients = ["a", "b", "c"];
restaurant.orderPasta(...ingredients); // a b c

rest 参数运算符和 ... 运算符写法几乎一致,但是却是执行相反的操作。

1
2
const [a, b, ...others] = [1, 2, 3, 4, 5];
console.log(a, b, others); //1 2 [ 3, 4, 5 ]

... 运算符配合使用:

1
2
3
4
5
const [pizza, , risotto, ...otherFoods] = [
...restaurant.mainMenu,
...restaurant.starterMenu,
];
console.log(pizza, risotto, otherFoods); //Pizza Risotto [ 'Focaccia', 'Bruschetta', 'Garlic Bread', 'Caprese Salad' ]

构造能处理任意参数的函数:

1
2
3
4
5
6
7
8
9
10
11
const add = function (...numbers) {
let sum = 0;
for (let number of numbers) {
sum += number;
}
console.log(sum);
};

add(2, 4); //6
add(2, 5, 8, 10); //25
add(1, 2, 3, 5, 7, 13, 17); //48

另一个函数使用 rest 参数的例子:

1
2
3
4
5
6
restaurant.orderPizza = function (mainIngredient, ...otherIngredients) {
console.log(mainIngredient);
console.log(otherIngredients);
};

restaurant.orderPizza("mushrooms", "onion", "olives", "spinach"); //[ 'onion', 'olives', 'spinach' ]

逻辑短路与分配运算符

&&||?? 可以实现逻辑短路。

  • || 划分的语句返回第一个真值语句的值,如果都是假值,则返回最后一个。

    1
    2
    3
    4
    5
    6
    7
    console.log(3 || "vonbrank"); //3
    console.log(false || 0); //0
    console.log(null || undefined); //undefiend
    console.log(undefined || null); //null

    console.log(0 || null || undefined || "" || "hello" || 23);
    // hello

    这种操作可以检测并初始化变量:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    restaurant.numGuests = 20;

    //方案1
    const guests1 = restaurant.numGuests ? restaurant.numGuests : 10;
    console.log(guests1); //20

    //方案2
    const guests2 = restaurant.numGuests || 10;
    console.log(guests2); //20

    //相比之下使用短路运算符更方便。
  • &&|| 相反,其返回第一个为假值的语句的返回值,如果都是真值,则返回最后一个。

    1
    2
    3
    console.log("vonbrank" && false);
    console.log(0 && "vonbrank");
    console.log(7 && "vonbrank");
  • ??|| 几乎一样,但是它只会跳过 nullundefined0""false 不受影响。

    1
    2
    3
    4
    5
    restaurant.numGuests = null;
    const guests2 = restaurant.numGuests ?? 10;
    console.log(guests2); //10

    console.log(undefined ?? 0 ?? "hello"); //0

短路运算符不仅仅会返回值,被其隔开的每一个语句在短路之前都将被执行。

1
2
3
4
5
if (restaurant.orderPizza) {
restaurant.orderPizza("mushrooms", "spinach");
}

restaurant.orderPizza && restaurant.orderPizza("mushrooms", "spinach");

上述 restaurant.orderPizza 为真值,所以 && 右边的语句被继续执行,实现了 if 的功能,但需要注意的是,我们不提倡滥用短路运算符乃至放弃使用 if 语句,因为滥用会影响代码的可读性。

因此短路运算符的实质是:执行被运算符隔开的每一条语句,直至短路,并返回最后一条语句的返回值。其中, || 被真值短路,&& 被假值短路,?? 被除了 nullundefined 外的值短路。

JavaScript ES2020 提供了全新的逻辑分配符: ||=&&=??=

可用于化简包含短路运算符的语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
restaurant1.numGuests = restaurant1.numGuests || 10;

restaurant1.numGuests ||= 10;
//等价于 restaurant1.numGuests = restaurant1.numGuests || 10;

restaurant2.numGuests ||= 10;
//等价于 restaurant2.numGuests = restaurant2.numGuests || 10;

console.log(restaurant1.numGuests); //10
console.log(restaurant2.numGuests); //10

restaurant2.owner &&= "<ANONYMOUS>";
//等价于 restaurant2.owner = restaurant2.owner && "<ANONYMOUS>"
console.log(restaurant2.owner);

此处需要注意,分配符左侧需要是有效左值,即可被赋值的变量。

高级循环

  • for-of 循环

    for-of 循环写法如下,可以免去自定义循环变量的麻烦:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const menu = [...restaurant.starterMenu, ...restaurant.mainMenu];

    for (const item of menu) {
    console.log(item);
    }
    // Focaccia
    // Bruschetta
    // Garlic Bread
    // Caprese Salad
    // Pizza
    // Pasta
    // Risotto

    for-of 依然可以使用索引:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    for (const [i, item] of menu.entries()) {
    console.log(`${i + 1}. ${item}`);
    }
    //1. Focaccia
    //2. Bruschetta
    //3. Garlic Bread
    //4. Caprese Salad
    //5. Pizza
    //6. Pasta
    //7. Risotto

    for-of 循环也适用于对象:

    Object.keys() 可以返回对象关键字的数组:

    1
    2
    3
    4
    5
    6
    for (const day of Object.keys(openingHours)) {
    console.log(day);
    }
    //Thu
    //Fri
    //Sat

    Object.values() 可以返回对象值的数组。

    Object.entries() 返回对象 [key, value] 的数组。

    一个灵活使用对象迭代器、解构赋值与 for-of 的例子:

    1
    2
    3
    4
    5
    6
    for (const [key, { open, close }] of Object.entries(openingHours)) {
    console.log(`On ${key}, we open at ${open} and close at ${close}`);
    }
    //On Thu, we open at 12 and close at 22
    //On Fri, we open at 11 and close at 23
    //On Sat, we open at 0 and close at 24

增强型对象字面值与可选链

JavaScript 对象成员的名称是可计算的,只需要用 [] 包裹即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

const openingHours = {
[`${weekdays[4]}`]: {
open: 12,
close: 22,
},
Fri: {
open: 11,
close: 23,
},
Sat: {
open: 0, // Open 24 hours
close: 24,
},
};
console.log(openingHours);
/*
{
Thu: { open: 12, close: 22 },
Fri: { open: 11, close: 23 },
Sat: { open: 0, close: 24 }
}
*/

实际项目中常常需要操作一个未知对象:

1
console.log(restaurant.openingHours.mon.open);

我们不确定 restaurant.openingHours 是否有 mon 这个成员,一旦没有,就会报异常。所以通常这么写:

1
2
3
if (restaurant.openingHours.mon) {
console.log(restaurant.openingHours.mon.open);
}

ES6 提供了可选链简化此类代码。

若不知道某个成员是否存在,可以在其后加一个 ? ,若不存在则直接返回 undefined ,防止访问形如 undefined.open 导致异常。

1
console.log(restaurant.openingHours.mon?.open); //undefined

可用来判断方法是否存在:

1
console.log(restaurant.order1?.(0, 1) ?? "Method does not exist"); //Method does not exist

判断对象成员是否存在:

1
2
3
4
5
6
const users = [
{ name: "vonbrank", job: "student" },
{ name: "alice", job: "Engineer" },
];
console.log(users[0]?.name || "user property does not exist"); ///vonbrank
console.log(users[2]?.name || "user property does not exist"); //user property does not exist

MapSet

和数学上的 集合 完全类似, JavaScript 中的 Set 是一种只允许任何元素出现一次的数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const orderSet = new Set([
"Pizza",
"Pasta",
"Pizza",
"Pizza",
"Risotto",
"Pasta",
"Pizza",
]);

console.log(orderSet); //Set(3) { 'Pizza', 'Pasta', 'Risotto' }
console.log(orderSet.size); //3
console.log(orderSet.has("Pizza")); //true
console.log(orderSet.has("Bread")); //false

orderSet.add("Garlic Bread");
console.log(orderSet); //Set(4) { 'Pizza', 'Pasta', 'Risotto', 'Garlic Bread' }
orderSet.delete("Risotto");
// orderSet.clear();
console.log(orderSet); //Set(3) { 'Pizza', 'Pasta', 'Garlic Bread' }

//Set 是可迭代对象,可使用 for-of 循环
for (const order of orderSet) {
console.log(order);
}

//Set 拆解字符串
console.log(new Set("hello, world!"));
//Set(10) { 'h', 'e', 'l', 'o', ',', ' ', 'w', 'r', 'd', '!' }

Map 可建立其两个元素的配对。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const restaurant3 = new Map();

restaurant3.set("name", "origus");
restaurant3.set(1, "Italy");
restaurant3.set(2, "Franch");
restaurant3
.set("categories", ["Italian", "Pizzeria", "Vegetarian", "Organic"])
.set("open", "11")
.set("close", "23")
.set(true, "we are open")
.set(false, "we are closed");

console.log(restaurant3);
/*
Map(8) {
'name' => 'origus',
1 => 'Italy',
2 => 'Franch',
'categories' => [ 'Italian', 'Pizzeria', 'Vegetarian', 'Organic' ],
'open' => '11',
'close' => '23',
true => 'we are open',
false => 'we are closed'
}
*/

console.log(restaurant3.get("name")); //origus
restaurant3.delete(2);
console.log(restaurant3);

console.log([...restaurant3.entries()]);
/*
[
[ 'name', 'origus' ],
[ 1, 'Italy' ],
[ 'categories', [ 'Italian', 'Pizzeria', 'Vegetarian', 'Organic' ] ],
[ 'open', '11' ],
[ 'close', '23' ],
[ true, 'we are open' ],
[ false, 'we are closed' ]
]
*/

console.log(restaurant3.has("categories")); //true
console.log(restaurant3.size); //7

字符串操作

JavaScript 提供许多操作字符串的方法。

  • 索引搜索与切片

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    const airline = "TAP Air Portugal";
    const plane = "A320";

    console.log(plane[0]); //A
    console.log("B747"[0]); //B

    //搜索字符串第一个索引位置,找不到返回 -1
    console.log(airline.indexOf("Portugal")); //10

    //搜索字符串最后一个索引位置
    console.log(airline.lastIndexOf("r")); //8

    //.slice 用于字符串切片
    console.log(airline.slice(5)); //ir Portugal
    console.log(airline.slice(4, 7)); //Air

    //字符串切片结合索引位置实现切片第一个单词
    console.log(airline.slice(0, airline.indexOf(" "))); //TAP
    //切片最后一个单词
    console.log(airline.slice(airline.lastIndexOf(" ") + 1)); //Portugal

    //负数切片表示倒数位置
    console.log(airline.slice(-2)); //al
    console.log(airline.slice(1, -1)); //AP Air Portuga

    实例:判断座位是不是在中间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const checkMiddleSeat = function (seat) {
    //B and E are middle seats
    const s = seat.slice(-1);
    if (s === "B" || s === "E") {
    console.log("You get a middle seat.");
    } else {
    console.log("You got lucky.");
    }
    };

    checkMiddleSeat("11B"); //You get a middle seat.
    checkMiddleSeat("23C"); //You got lucky.
    checkMiddleSeat("3E"); //You get a middle seat.
  • 大小写转换、去除空白字符、替换、前后缀判断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    const airline = "TAP Air Portugal";

    //大小写转换
    console.log(airline.toUpperCase());
    console.log(airline.toLowerCase());

    //字符串格式化转为首字母大写
    const passenger = "vONBrNk";
    const passengerLower = passenger.toLowerCase();
    const passengerCorrect =
    passenger[0].toUpperCase() + passengerLower.slice(1);
    console.log(passengerCorrect);

    //格式化邮箱,去除末尾回车字符
    const email = "hello@vonbrank.com";
    const emailLogin = "HelLo@vOnBRnk.CoM \n";
    const emailLower = emailLogin.toLowerCase();
    const emailTrimmed = emailLower.trim();
    console.log(emailTrimmed);

    //字符串方法均返回新字符串引用,因此可以连续调用
    const emailNormalized = emailLogin.toLowerCase().trim();
    console.log(emailNormalized);
    console.log(emailTrimmed === emailNormalized);

    // .replace(d, t) 可以把第一个 d 替换为 t
    const priceGB = "288,97£";
    const priceUS = priceGB.replace("£", "$").replace(",", ".");
    console.log(priceUS);

    const announcements =
    "All passengers come to boarding door 23. Boarding door 23!";
    console.log(announcements.replace("door", "gate"));

    //ReplaceAll 是 ES2021 新特性
    console.log(announcements.replaceAll("door", "gate"));

    //使用正则表达式可以实现仅用 replace 方法替换所有字符串
    console.log(announcements.replace(/door/g, "gate"));

    const plane = "Airbus A320neo";
    console.log(plane.includes("A320")); //true
    console.log(plane.includes("Boeing")); //false
    // console.log(plane.startsWith("A3")); //true
    console.log(plane.startsWith("Air")); //false

    一个例子,实现违禁品判断:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    if (plane.startsWith("Airbus") && plane.endsWith("neo")) {
    console.log("Part of the new Airbus family");
    }

    const checkBaggage = function (items) {
    const baggage = items.toLowerCase();
    if (baggage.includes("knife") || baggage.includes("gun")) {
    console.log("You are not allowed on board.");
    } else console.log("Welcome aboard.");
    };

    checkBaggage("I have a laptop, some Food and a pocket Knife.");
    checkBaggage("Socks and camera");
    checkBaggage("Got some snacks and a gun for protection");
  • 字符串分割与合并、追加与扩充、倍增

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //split 用来分割字符串,传入需要分割的字符
    console.log("a+very+nice+string".split("+"));
    //[ 'a', 'very', 'nice', 'string' ]

    console.log("von brank".split(" "));
    //[ 'von', 'brank' ]

    const [firstName, lastName] = "von brank".split(" ");
    console.log(firstName, lastName);

    //join 可将数组里的字符串用传入字符连接
    const newName = ["Mr.", firstName, lastName.toUpperCase()].join(" ");
    console.log(newName); // Mr. von BRANK

    一个将首字母转大写的例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const capitalizeName = function (name) {
    const names = name.split(" ");
    const capitalizedName = [];
    for (const n of names) {
    // n[0] && capitalizedName.push(n[0].toUpperCase() + n.slice(1));
    n[0] && capitalizedName.push(n.replace(n[0], n[0].toUpperCase()));
    }
    return capitalizedName.join(" ");
    };

    const passenger = "jassica ann smith davis";
    console.log(capitalizeName(passenger));
    //Jassica Ann Smith Davis

    padStartpadEnd 可以实现在字符串前后或填充字符至指定长度:

    1
    2
    3
    4
    5
    const message = "Go to gate 23";
    console.log(message.padStart(25, "+"));
    //++++++++++++Go to gate 23
    console.log("vonbrank".padEnd(23, "+"));
    //vonbrank+++++++++++++++

    一个创建掩码的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    const maskCreditCard = function (number) {
    const str = String(number);
    const last = str.slice(-4);
    return last.padStart(str.length, "*");
    };

    console.log(maskCreditCard(14285731415926));
    //**********5926

    string.repeat(n) 可以实现将 string 重复 n

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const message2 = "Bad weather... All departures delayed...\n";
    console.log(message2.repeat(5));
    /*
    Bad weather... All departures delayed...
    Bad weather... All departures delayed...
    Bad weather... All departures delayed...
    Bad weather... All departures delayed...
    Bad weather... All departures delayed...
    */

    const planesInLine = function (n) {
    console.log(`There are ${n} planes in line. ${"✈️".repeat(n)}`);
    };

    planesInLine(5); //There are 5 planes in line. ✈️✈️✈️✈️✈️
    planesInLine(3); //There are 3 planes in line. ✈️✈️✈️

深入理解 JavaScript 函数

默认参数与引用传递

ES6 的新特性之一就是允许函数设置默认参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const bookings = [];
const createBooking = function (
flightNum,
numPassengers = 1,
price = 100 * numPassengers
) {
//ES5 way
// numPassengers ||= 1;
// price ||= 100;
const booking = {
flightNum,
numPassengers,
price,
};
console.log(booking);
bookings.push(booking);
};

createBooking("CZ6369");
createBooking("CZ6371", undefined, 200);
createBooking("CZ6371", 10, 399);
/*
{ flightNum: 'CZ6369', numPassengers: 1, price: 100 }
{ flightNum: 'CZ6371', numPassengers: 1, price: 200 }
{ flightNum: 'CZ6371', numPassengers: 10, price: 399 }
*/

函数参数如果是基本类型,则传入值,对象传入引用,因此在函数内修改对象将使得原对象被修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const flight = "CZ6371";
const vonbrank = {
name: "Von Brank",
passport: 1234567,
};

const checkIn = function (flightNum, passenger) {
flightNum = "CZ6666";
passenger.name = "Mr." + passenger.name;

if (passenger.passport === 1234567) {
console.log("check in");
} else console.log("wrong passport");
};

checkIn(flight, vonbrank);
console.log(flight, vonbrank);
// CZ6371 { name: 'Mr.Von Brank', passport: 1234567 }

高阶函数与回调函数

JavaScript 的重要特性之一就是所谓的 Firt-Class Function ,即函数与普通变量同等看待,有 First-Class Function 便有 High Order Function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const oneWord = function (str) {
return str.replace(/ /g, "").toLowerCase();
};

const upperFirstWord = function (str) {
const [first, ...other] = str.split(" ");
return [first.toUpperCase(), ...other].join(" ");
};

const transformer = function (str, fn) {
console.log(`Original string: ${str}`);
console.log(`Transformed string: ${fn(str)}`);

console.log(`Transformed by: ${fn.name}`);
};

transformer("JavaScript is the best language!", upperFirstWord);
/*
Original string: JavaScript is the best language!
Transformed string: JAVASCRIPT is the best language!
Transformed by: upperFirstWord
*/

transformer("JavaScript is the best language!", oneWord);
/*
Original string: JavaScript is the best language!
Transformed string: javascriptisthebestlanguage!
Transformed by: oneWord
*/

这个例子展示了 transformer 接收一个 string 和一个 fntransformer 不在乎 fn 将以怎样的方式处理 string ,只需要 fn 接收 str 处理并返回一个值即可, transformer 内部将调用 fn 传入 string 并执行。

这里的 fn 便是所谓的回调函数(call back function) ,意思是把函数当作值传入,稍后再调用。

JavaScript 总是以回调的形式执行函数;forEachaddEventListener 也是接受回调函数的高阶函数:

1
2
3
4
5
6
7
8
9
//JavaScript used callback all the time
const high5 = function (item) {
console.log("Hooray!");
console.log(item);
};

document.body.addEventListener("click", high5);

["VonBrank", "Alice", "Bob", "Cathy"].forEach(high5);

callapplybind 方法

通常,对对象的成员函数,我们可以用 . 运算符来直接执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const nanHang = {
airline: "nanHang",
iataCode: "CZ",
bookings: [],
book: function (flightNum, passengerName) {
console.log(
`${passengerName} booked a seat on ${this.airline} flight ${this.iataCode}${flightNum}.`
);
this.bookings.push({
flight: `${this.iataCode}${flightNum}`,
name: passengerName,
});
},
};

nanHang.book("6371", "VonBrank");
nanHang.book("6317", "Smith");
console.log(nanHang);
/*
VonBrank booked a seat on nanHang flight CZ6371.
Smith booked a seat on nanHang flight CZ6317.
{
airline: 'nanHang',
iataCode: 'CZ',
bookings: [
{ flight: 'CZ6371', name: 'VonBrank' },
{ flight: 'CZ6317', name: 'Smith' }
],
book: [Function: book]
}
*/

前文提到过,函数执行上下文中的 this 在不同调用条件下可能是不一样的。

如果我们想要把方法保存到另外的变量里,单独执行将导致 this 变成 defined,除了手动传入 this 参数,我们没有其他方法指定 this

因此 JavaScript 提供了 callapplybind 三种方法便于开发者手动指定 this 变量的值。

call 方法的第一个参数指定的的是函数执行时的 this 变量,之后依照调用函数的写法用逗号隔开所有传入的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const chuanHang = {
airline: "chuangHang",
iataCode: "3U",
bookings: [],
};

const book = nanHang.book;

book.call(chuanHang, "3456", "Steven");
console.log(chuanHang);
/*
Steven booked a seat on chuangHang flight 3U3456.
{
airline: 'chuangHang',
iataCode: '3U',
bookings: [ { flight: '3U3456', name: 'Steven' } ]
}

*/

apply 方法与 call 略有不同,对于函数原本接收的参数,需要以数组的形式传入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const shenHang = {
airline: "Shenzhen Airline",
iataCode: "ZH",
bookings: [],
};

book.call(shenHang, 538, "Alice");
console.log(shenHang);

const flightData = [583, "Cooper"];
book.apply(shenHang, flightData);
console.log(shenHang);
/*
Alice booked a seat on Shenzhen Airline flight ZH538.
Cooper booked a seat on Shenzhen Airline flight ZH583.
{
airline: 'Shenzhen Airline',
iataCode: 'ZH',
bookings: [
{ flight: 'ZH538', name: 'Alice' },
{ flight: 'ZH583', name: 'Cooper' }
]
}
*/

bindthis 等参数的绑定与函数的执行分开:

1
2
3
4
5
const book3U = book.bind(chuanHang);
book3U(1234, "Bob"); //Bob booked a seat on chuangHang flight 3U1234.

const bookCZ6371 = book.bind(nanHang, 6371);
bookCZ6371("Cathy"); //Cathy booked a seat on nanHang flight CZ6371.

本例中 const book3U = book.bind(chuanHang);book3Uthis 变量永久钦定为 chuanHang 之后单独执行 book3U 时就像直接执行 chuanHang.book 一样; const bookCZ6371 = book.bind(nanHang, 6371); 则不仅绑定了 this 参数,还绑定了 flightNum 参数,之后 bookCZ6371("Cathy") 相当于原本执行 nanHang.book(6371, "Cathy");

bind 还可以用在 addEventListener 中传入的回调函数是某个对象的方法,通过 bind 指定方法的 this 来防止出错。

自执行匿名函数与闭包

想要创建一个独立的作用域,执行某些代码块,是编程中常见的需求, ES5 前常使用 Immediately Invoke Function Expression (IIFE) 实现:

1
2
3
4
(functino () {

console.log(`This function executes only once.`);
})();

现代 JavaScript 的做法是使用 letconst 变量将作用域限制在块级,然后使用单一的大括号创建代码块实现:

1
2
3
{
console.log(`This function executes only once.`);
}

闭包(closure)是 JavaScript 中最容易让初学者感到困惑的东西。

先看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const greet = function (greeting) {
return function (name) {
console.log(`${greeting} ${name}!`);
};
};

const greeterHey = greet("Hey");
greeterHey("Jonas"); //Hey Jonas
greeterHey("Von Brank"); //Hey Von Brank

greet("Hello")("Alice"); //Hello Alice

const greetArrow = (greeting) => (name) => console.log(`${greeting} ${name}!`);

const greeterArrowHey = greetArrow("Hey");
greeterArrowHey("Jonas");

greeting 接收一个参数,返回的却是一个函数;本例中 greeterHey 就是一个从 greet 返回的函数,既然它是函数,就是可执行的。

问题在于, 执行 greeterHey 时需要访问 greet 函数中的 greeting 变量,而 greet 函数的上下文在此时早已结束,被弹出调用栈了,那这里的 greeting 是哪来的? What the hell is it?

实现这一切的便是 JavaScript 的闭包机制!闭包的原理解释如图所示:

note-javascript-closure

如果函数返回的是另外一个函数,那么返回的函数会有一个成员变量,名为 closure ,里面包含了创建这个函数位置所处作用域内的所有变量的引用,当携带闭包的函数被执行时,若任何不在其作用域内的变量被访问,JavaScript 引擎都会先去闭包里搜索该变量,若找不到才接着到作用域链里搜索。

嵌套的箭头函数也是闭包:

1
const greetArrow = (greeting) => (name) => console.log(`${greeting} ${name}!`);

数组的使用

数组基础方法

slicesplice 用于数组切片,与字符串切片类似,不同之处在于, splice 将直接修改原数组,而 slice 不会:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let arr = ["a", "b", "c", "d", "e", "f", "g"];
console.log(arr.slice(2, 4)); //[ 'c', 'd' ]
console.log(arr.slice(3)); //[ 'd', 'e', 'f', 'g' ]
console.log(arr.slice(-1)); //[ 'g' ]
console.log(arr.slice(1, -2)); //[ 'b', 'c', 'd', 'e' ]
console.log(arr.slice());
/*
[
'a', 'b', 'c',
'd', 'e', 'f',
'g'
]
*/

arr = ["a", "b", "c", "d", "e", "f", "g"];
console.log(arr.splice(2)); // ["c", "d", "e", "f", "g"]
console.log(arr); // ["a", "b"]
//splice 会移除元素中中使用 splice 切片的部分

reverse 用于数组翻转,会直接修改原数组

1
2
3
4
const arr = ["a", "b", "c", "d", "e", "f", "g"];
const arr2 = ["h", "i", "j", "k", "l"];
console.log(arr2.reverse()); //[ 'l', 'k', 'j', 'i', 'h' ]
console.log(arr2); //[ 'l', 'k', 'j', 'i', 'h' ]

concat 用于拼接数组, a.concat(b) 表示将 b 拼接在 a 后面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const arr = ["a", "b", "c", "d", "e", "f", "g"];
const arr2 = ["h", "i", "j", "k", "l"];
const letters = arr.concat(arr2);
console.log(letters);
/*
[
'a', 'b', 'c', 'd',
'e', 'f', 'g', 'h',
'i', 'j', 'k', 'l'
]
*/
console.log([...arr, ...arr2]);
/*
[
'a', 'b', 'c', 'd',
'e', 'f', 'g', 'h',
'i', 'j', 'k', 'l'
]
*/

join 可以用指定符号连接数组中所有元素,并连接成字符串:

1
2
const arr = ["a", "b", "c", "d", "e", "f", "g"];
console.log(arr.join("-")); //a-b-c-d-e-f-g

全新的 .at() 函数可以用于取代数组下标, arr.at(i) 返回第 i 个元素:

1
2
3
4
5
6
7
8
9
const arr = [23, 11, 64];
console.log(arr[0]); //23
console.log(arr.at(0)); //23
console.log(arr[arr.length - 1]); //64
console.log(arr.slice(-1)[0]); //64
console.log(arr.at(-1)); //64

//字符串也可以用
console.log("vonbrank".at(-1)); // 'k'

forEach 方法

遍历数组进行某种操作是一种常见的需求,通常使用 for 循环实现, JavaScript 提供了 forEach 函数简化代码:

1
2
3
4
5
6
7
const arr = ["USD", "EUR", "GBP", "CNY", "USD", "EUR", "USD"];

arr.forEach((value, i, arr) => {
console.log(value, i);
});

for (let i = 0; i < arr.length; i++) console.log(arr[i], i);

可以看出 forEach 方法传入一个回调函数,接收三个参数:值、索引和数组本身的引用。实现相同的功能 for 可能需要更多代码。

forEach 可以直接用于 SetMap 等可迭代对象,不同之处只是回调函数的参数而已:

  • Map: fn : (value, key, map) => {}
  • Set: fn : (value, value, set) => {}

注意因为 Set 没有 ArrayindexMapkey 之类的东西,所以传入的都是 value

node.insertAdjacentHTML() 方法可以往 DOM 树的 node 节点中插入文本,这些内容同时更新为新的 DOM 元素:

1
<div class="movements__row"></div>
1
2
3
4
5
6
7
8
9
10
11
const movementsRow = document.querySelector(".movements__row");
const type = "abc";
const i = 0;
const mov = 1000;
const html = `
<div class="movements__type movements__type--${type}">
${i + 1} ${type.toUpperCase()}
</div>
<div class="movements__value">${mov}€</div>
`;
movementsRow.insertAdjacentHTML(`afterbegin`, html);
1
2
3
4
<div class="movements__row">
<div class="movements__type movements__type--ABC">1 ABC</div>
<div class="movements__value">1000€</div>
</div>

insertAdjacentHTML 本身第一个参数有四种取值,除了 afterbegin 外,其他用法效果如图所示:

1
2
3
4
5
6
7
<!-- beforebegin -->
<p>
<!-- afterbegin -->
foo
<!-- beforeend -->
</p>
<!-- afterend -->

JavaScriip 实现面向对象编程 (OOP)

传统 OOP 中,我们使用 class 定义对象的模板,但是 JavaScript 中没有真正的 class ,即使是 ES6 中的 class 关键字也不过是语法糖而已,JavaScript 使用原型(prototype)来实现面向对象编程。

构造函数与 new 运算符

JavaScript 中的对象构造函数只能用显式声明函数和匿名函数表达式定义:

1
2
3
4
const Person = function (firstName, birthYear) {
this.firstName = firstName;
this.birthYear = birthYear;
};

注意不能使用箭头函数,因为众所周知箭头函数没有 this 变量。

当拥有构造函数后,我们就拥有了创建对象的模板,基于模板创建对象的过程称为“实例化”:

1
2
3
4
5
6
const jonas = new Person("Jonas", 1991);
console.log(jonas); //Person { firstName: 'Jonas', birthYear: 1991 }

const jassica = new Person("Jassica", 2017);
const jack = new Person("Jack", 1975);
console.log(jassica, jack); //Person { firstName: 'Jassica', birthYear: 2017 } Person { firstName: 'Jack', birthYear: 1975 }

可以使用 instanceof 关键字判断一个对象是不是某个构造函数的实例:

1
console.log(jonas instanceof Person); //true

对象原型 (Prototype)

有了对象,自然就要定义操作对象的方法,我们虽然可以这样定义:

1
2
3
4
5
6
7
const Person = function (firstName, birthYear) {
this.firstName = firstName;
this.birthYear = birthYear;
this.calcAge = function () {
return 2037 - birthYear;
};
};

但是实际项目中千万不要这样做,否则每次实例化都会创建一个新的函数对象,造成极大的性能浪费。

JavaScript 使用原型来定义方法。