JavaScript学习

JavaScriptJS)是一种具有函数优先特性的轻量级、解释型或者说即时编译型的编程语言。虽然作为 Web 页面中的脚本语言被人所熟知,但是它也被用到了很多非浏览器环境中,例如 Node.jsApache CouchDBAdobe Acrobat 等。进一步说,JavaScript 是一种基于原型、多范式、单线程动态语言,并且支持面向对象、命令式和声明式(如函数式编程)风格。

函数优先:指的是当一门编程语言的函数可以被当作变量一样用时,则称这门语言拥有头等函数

即时编译:在计算机技术中,即时编译(英语:Just-in-time compilation,缩写为JIT;又译及时编译实时编译),也称为动态翻译运行时编译,是一种执行计算机代码的方法,这种方法设计在程序执行过程中(在执行期)而不是在执行之前进行编译。通常,这包括源代码或更常见的字节码机器码的转换,然后直接执行。实现JIT编译器的系统通常会不断地分析正在执行的代码,并确定代码的某些部分,在这些部分中,编译或重新编译所获得的加速将超过编译该代码的开销。

JIT编译是两种传统的机器代码翻译方法——提前编译(AOT)和解释器——的结合,它结合了两者的优点和缺点。大致来说,JIT编译,以解释器的开销以及编译和链接(解释之外)的开销,结合了编译代码的速度与解释的灵活性。JIT编译是动态编译的一种形式,允许自适应优化,比如动态重编译和特定于微架构的加速——因此,在理论上,JIT编译比静态编译能够产生更快的执行速度。解释和JIT编译特别适合于动态编程语言,因为运行时系统可以处理后期绑定的数据类型并实施安全保证。

JavaScript 调用策略

要让脚本调用的时机符合预期,需要解决一系列的问题。这里看似简单,实际大有文章。最常见的问题就是:HTML 元素是按其在页面中出现的次序调用的,如果用 JavaScript 来管理页面上的元素(更精确的说法是使用 文档对象模型 DOM),若 JavaScript 加载于欲操作的 HTML 元素之前,则代码将出错。

浏览器遇到 async 脚本时不会阻塞页面渲染,而是直接下载然后运行。这样脚本的运行次序就无法控制,只是脚本不会阻止剩余页面的显示。当页面的脚本之间彼此独立,且不依赖于本页面的其他任何脚本时,async 是最理想的选择。

  • 如果脚本无需等待页面解析,且无依赖独立运行,那么应使用 async
  • 如果脚本需要等待页面解析,且依赖于其他脚本,调用这些脚本时应使用 defer,将关联的脚本按所需顺序置于 HTML 中。

数据类型

最新的 ECMAScript 标准定义了 8 种数据类型:

  • 七种基本数据类型:
    • 布尔值(Boolean),有 2 个值分别是:truefalse.
    • null,一个表明 null 值的特殊关键字。JavaScript 是大小写敏感的,因此 nullNullNULL或变体完全不同。
    • undefined,和 null 一样是一个特殊的关键字,undefined 表示变量未赋值时的属性。
    • 数字(Number),整数或浮点数,例如: 42 或者 3.14159
    • 任意精度的整数 (BigInt) ,可以安全地存储和操作大整数,甚至可以超过数字的安全整数限制。
    • 字符串(String),字符串是一串表示文本值的字符序列,例如:”Howdy” 。
    • 代表(Symbol)( 在 ECMAScript 6 中新添加的类型).。一种实例是唯一且不可改变的数据类型。
  • 以及对象(Object)。

语法和数据结构

变量声明

JavaScript 是区分大小写的,并使用 Unicode 字符集

JavaScript 有三种声明方式。

  • var

    声明一个变量,可选初始化一个值。

  • let

    声明一个块作用域的局部变量,可选初始化一个值。

  • const

    声明一个块作用域的只读常量。

区别:

var:

  • 用 var 声明的变量的作用域是它当前的执行上下文,即如果是在任何函数外面,则是全局执行上下文,如果在函数里面,则是当前函数执行上下文。换句话说,var 声明的变量的作用域只能是全局或者整个函数块的。
  • 存在变量提升:你可以先使用变量稍后再声明变量而不会引发异常。这一概念称为变量提升;JavaScript 变量感觉上是被“提升”或移到了函数或语句的最前面。但是,提升后的变量将返回 undefined 值。因此在使用或引用某个变量之后进行声明和初始化操作,这个被提升的变量仍将返回 undefined 值。
  • 一个变量可多次声明,后面的声明会覆盖前面的声明
  • var 在全局环境声明变量,会在全局对象里新建一个属性

let:

  • let 声明的变量的作用域则是它当前所处代码块,即它的作用域既可以是全局或者整个函数块,也可以是 if、while、switch等用{}限定的代码块。
  • 不存在变量提升,let声明变量前,该变量不能使用(暂时性死区)
  • 不允许在同一作用域中重复声明,否则将抛出异常。let 声明的重复性检查是发生在词法分析阶段,也就是在代码正式开始执行之前就会进行检查。
  • let 在全局环境声明变量,则不会在全局对象里新建一个属性。

const:

常量不可以通过重新赋值改变其值,也不可以在代码运行时重新声明。它必须被初始化为某个值。常量的作用域规则与 let 块级作用域变量相同。

变量赋值

varlet 语句声明的变量,如果没有赋初始值,则其值为 undefined

可以使用 undefined 来判断一个变量是否已赋值。在以下的代码中,变量input未被赋值,因此 if (en-US) 条件语句的求值结果是 true

1
2
3
4
5
6
var input;
if(input === undefined){
doThis();
} else {
doThat();
}
  • undefined 值在布尔类型环境中会被当作 false, 数值类型环境中 undefined 值会被转换为 NaN
  • null 在数值类型环境中会被当作 0 来对待,而布尔类型环境中会被当作 false

Javascript声明提升

Js中,函数及其变量的声明都将被提升到函数的最顶部。也就是可以先使用后声明。

只有声明会被提升,但是初始化不会

1
2
var x; //声明,会被提升
var x = 5; //初始化,不会提升

现代模式

长久以来,JavaScript 不断向前发展且并未带来任何兼容性问题。新的特性被加入,旧的功能也没有改变。

这么做有利于兼容旧代码,但缺点是 JavaScript 创造者的任何错误或不完善的决定也将永远被保留在 JavaScript 语言中。

这种情况一直持续到 2009 年 ECMAScript 5 (ES5) 的出现。ES5 规范增加了新的语言特性并且修改了一些已经存在的特性。为了保证旧的功能能够使用,大部分的修改是默认不生效的。你需要一个特殊的指令 —— "use strict" 来明确地激活这些特性。

请确保 "use strict" 出现在脚本的最顶部,否则严格模式可能无法启用。

现代 JavaScript 支持 “class” 和 “module” —— 高级语言结构(本教程后续章节会讲到),它们会自动启用 use strict。因此,如果我们使用它们,则无需添加 "use strict" 指令。

错误处理

JavaScript 可以抛出任意对象。然而,不是所有对象能产生相同的结果。尽管抛出数值或者字母串作为错误信息十分常见,但是通常用下列其中一种异常类型来创建目标更为高效:

try...catch 语句标记一块待尝试的语句,并规定一个以上的响应应该有一个异常被抛出。如果我们抛出一个异常,try...catch语句就捕获它。

try...catch 语句有一个包含一条或者多条语句的 try 代码块,0 个或 1 个的catch代码块,catch 代码块中的语句会在 try 代码块中抛出异常时执行。换句话说,如果你在 try 代码块中的代码如果没有执行成功,那么你希望将执行流程转入 catch 代码块。如果 try 代码块中的语句(或者try 代码块中调用的方法)一旦抛出了异常,那么执行流程会立即进入catch 代码块。如果 try 代码块没有抛出异常,catch 代码块就会被跳过。finally 代码块总会紧跟在 try 和 catch 代码块之后执行,但会在 try 和 catch 代码块之后的其他代码之前执行。

下面的例子使用了try...catch语句。示例调用了一个函数用于从一个数组中根据传递值来获取一个月份名称。如果该值与月份数值不相符,会抛出一个带有"InvalidMonthNo"值的异常,然后在捕捉块语句中设monthName变量为unknown

try..catch…finally示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function f() {
try {
console.log(0);
throw "bogus";
} catch(e) { //无条件的catch块
console.log(1);
return true; // this return statement is suspended
// until finally block has completed
console.log(2); // not reachable
} finally {
console.log(3);
return false; // overwrites the previous "return"
console.log(4); // not reachable
}
// "return false" is executed now
console.log(5); // not reachable
}
f(); // console 0, 1, 3; returns false

抛出对象可以是任何一个对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try {
try {
throw {toString: function() { return "I'm an object!"; } };
}
catch (ex) {
console.error(ex.toString());
throw ex;
}
finally {
console.log("finally");
return;
}
}
catch (ex) {
console.error("outer", ex.message); //得不到执行
}

函数

函数是一个值。重申一次:无论函数是如何创建的,函数都是一个值。

  • 函数声明:在主代码流中声明为单独的语句的函数:

    1
    2
    3
    4
    // 函数声明
    function sum(a, b) {
    return a + b;
    }
  • 函数表达式:在一个表达式中或另一个语法结构中创建的函数。下面这个函数是在赋值表达式 = 右侧创建的:

    1
    2
    3
    4
    // 函数表达式
    let sum = function(a, b) {
    return a + b;
    };

函数表达式是在代码执行到达时被创建,并且仅从那一刻起可用。函数声明则不同。在函数声明被定义之前,它就可以被调用。函数声明的另外一个特殊的功能是它们的块级作用域。严格模式下,当一个函数声明在一个代码块内时,它在该代码块内的任何位置都是可见的。但在代码块外不可见。

闭包

闭包(Closure)是指在 JavaScript 中能够访问独立的函数作用域的函数,即使在该函数外部调用。闭包可以访问其自身作用域以及包含它的函数作用域。这是由于 JavaScript 的词法作用域规则,函数在定义时就“记住”了它被创建的作用域。

闭包通常在以下情况下发挥作用:

  1. 封装变量: 通过闭包,可以创建私有变量,这些变量对外部是不可见的,从而实现一定程度的封装。
  2. 保存状态: 闭包可以用于保存函数执行时的状态,使得函数在后续调用时可以继续操作之前的状态。

你可以在一个函数里面嵌套另外一个函数。嵌套(内部)函数对其容器(外部)函数是私有的。它自身也形成了一个闭包。一个闭包是一个可以自己拥有独立的环境与变量的表达式(通常是函数)。

既然嵌套函数是一个闭包,就意味着一个嵌套函数可以”继承“容器函数的参数和变量。换句话说,内部函数包含外部函数的作用域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function createCounter() {
let count = 0;
return {
increment: function() {
count++;
console.log(count);
},
reset: function() {
count = 0;
console.log("Counter reset");
}
};
}

const counterObj = createCounter();
counterObj.increment(); // 输出 1
counterObj.increment(); // 输出 2
counterObj.reset(); // 输出 "Counter reset"

闭包是 JavaScript 中最强大的特性之一。JavaScript 允许函数嵌套,并且内部函数可以访问定义在外部函数中的所有变量和函数,以及外部函数能访问的所有变量和函数。

但是,外部函数却不能够访问定义在内部函数中的变量和函数。这给内部函数的变量提供了一定的安全性。

此外,由于内部函数可以访问外部函数的作用域,因此当内部函数生存周期大于外部函数时,外部函数中定义的变量和函数的生存周期将比内部函数执行时间长。当内部函数以某一种方式被任何一个外部函数作用域访问时,一个闭包就产生了。

箭头函数

1
let func = (arg1, arg2, ..., argN) => expression;

到目前为止,我们看到的箭头函数非常简单。它们从 => 的左侧获取参数,计算并返回右侧表达式的计算结果。

有时我们需要更复杂一点的函数,比如带有多行的表达式或语句。在这种情况下,我们可以使用花括号将它们括起来。主要区别在于,用花括号括起来之后,需要包含 return 才能返回值(就像常规函数一样)。

就像这样:

1
2
3
4
let sum = (a, b) => {  // 花括号表示开始一个多行函数
let result = a + b;
return result; // 如果我们使用了花括号,那么我们需要一个显式的 “return”
};

CMAScript 2015,也称 ES6,引入了 JavaScript 类。JavaScript 类是 JavaScript 对象的模板。请使用关键字 class 创建类。请始终添加名为 constructor() 的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ClassName {
constructor() { ... }
method_1() { ... }
method_2() { ... }
method_3() { ... }
}

//示例
class Car {
constructor(name, year) {
this.name = name;
this.year = year;
}
age(x) {
return x - this.year;
}
}

对象

对象(object)是 JavaScript 语言的核心概念,也是最重要的数据类型。对象的所有键名都是字符串,(ES6又引入Symbol值也可以作为键名),所以加不加引号都可以。

创建对象

我们可以用下面两种语法中的任一种来创建一个空的对象:

1
2
let user = new Object(); // “构造函数” 的语法
let user = {}; // “字面量” 的语法

字面量方式:

1
2
3
4
5
6
7
8
9
var person = {
firstName: "John",
lastName : "Doe",
id : 5566,
fullName : function()
{
return this.firstName + " " + this.lastName;
}
};

使用构造函数创建对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义一个构造函数
function Person(name, age) {
// 使用 this 关键字初始化对象的属性
this.name = name;
this.age = age;

// 可以在构造函数中定义方法
this.sayHello = function() {
console.log("Hello, my name is " + this.name + " and I am " + this.age + " years old.");
};
}

// 使用构造函数创建对象
var person1 = new Person("John", 25);
var person2 = new Person("Jane", 30);

// 调用对象的方法
person1.sayHello(); // 输出: Hello, my name is John and I am 25 years old.
person2.sayHello(); // 输出: Hello, my name is Jane and I am 30 years old.

访问属性

1
2
person.age;
person["age"];
  • 我们经常使用点表示法(dot notation)来访问对象的属性和方法

  • 另一种访问对象属性的方式是使用括号表示法(bracket notation)

this

通常,对象方法需要访问对象中存储的信息才能完成其工作。例如,user.sayHi() 中的代码可能需要用到 user 的 name 属性。为了访问该对象,方法中可以使用 this 关键字。this 的值就是在点之前的这个对象,即调用该方法的对象。

1
2
3
4
5
6
7
8
9
10
11
let user = {
name: "John",
age: 30,

sayHi() {
// "this" 指的是“当前的对象”
alert(this.name);
}

};
user.sayHi(); // John

this不受限制

在 JavaScript 中,this 关键字与其他大多数编程语言中的不同。JavaScript 中的 this 可以用于任何函数,即使它不是对象的方法。

下面这样的代码没有语法错误

1
2
3
function sayHi() {
alert( this.name );
}

this 的值是在代码运行时计算出来的,它取决于代码上下文。

例如,这里相同的函数被分配给两个不同的对象,在调用中有着不同的 “this” 值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let user = { name: "John" };
let admin = { name: "Admin" };

function sayHi() {
alert( this.name );
}

// 在两个对象中使用相同的函数
user.f = sayHi;
admin.f = sayHi;

// 这两个调用有不同的 this 值
// 函数内部的 "this" 是“点符号前面”的那个对象
user.f(); // John(this == user)
admin.f(); // Admin(this == admin)

admin['f'](); // Admin(使用点符号或方括号语法来访问这个方法,都没有关系。)

解除 this 绑定的后果

如果你经常使用其他的编程语言,那么你可能已经习惯了“绑定 this”的概念,即在对象中定义的方法总是有指向该对象的 this。在 JavaScript 中,this 是“自由”的,它的值是在调用时计算出来的,它的值并不取决于方法声明的位置,而是取决于在“点符号前”的是什么对象。在运行时对 this 求值的这个概念既有优点也有缺点。一方面,函数可以被重用于不同的对象。另一方面,更大的灵活性造成了更大的出错的可能。这里我们的立场并不是要评判编程语言的这个设计是好是坏。而是要了解怎样使用它,如何趋利避害。

箭头函数没有自己的 this。箭头函数有些特别:它们没有自己的 this。如果我们在这样的函数中引用 thisthis 值取决于外部“正常的”函数。

对象原型

原型是 JavaScript 对象相互继承特性的机制。在这篇文章中,我们将解释什么是原型,原型链如何工作,以及如何为一个对象设置原型。JavaScript 中所有的对象都有一个内置属性,称为它的 prototype(原型)。它本身是一个对象,故原型对象也会有它自己的原型,逐渐构成了原型链。原型链终止于拥有 null 作为其原型的对象上。

在 JavaScript 中,有多种设置对象原型的方法,这里我们将介绍两种:Object.create() 和构造函数。

Object.create()

在 JavaScript 的早期版本中,确实没有类的概念,只有构造器函数和原型继承。然而,自 ECMAScript 2015(通常称为 ES6)引入之后,JavaScript 提供了对类和面向对象编程的支持。

在 ES6 中,你可以使用 class 关键字来定义类,以更直观的方式创建对象。类提供了一种更结构化和面向对象的编程方式,其中可以定义构造函数、实例方法、静态方法等。

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
class Person {
name; // 定义 name 属性
age; // 定义 age 属性

constructor(name, age) {
this.name = name; // 初始化 name 属性
this.age = age; // 初始化 age 属性
}

sayHello() {
console.log(`Hello, my name is ${this.name}`);
}
}

// 使用类创建对象实例
const person1 = new Person("Alice", 25);
const person2 = new Person("Bob", 30);

console.log(person1.name); // 输出: "Alice"
console.log(person1.age); // 输出: 25

console.log(person2.name); // 输出: "Bob"
console.log(person2.age); // 输出: 30

// 调用实例方法
person1.sayHello(); // 输出: "Hello, my name is Alice"

Symbol

symbol 有两个主要的使用场景:

  1. 创建唯一的对象属性名:由于 Symbol 的值是唯一的,可以用作对象属性的键,确保属性名的唯一性,避免命名冲突的问题。

    1
    2
    3
    4
    5
    6
    const KEY = Symbol();
    const obj = {};

    obj[KEY] = 'value';

    console.log(obj[KEY]); // 输出: 'value'
  2. 用作常量定义:通过使用 Symbol 创建常量,可以避免使用字符串或其他类型的值,从而确保常量的唯一性。

  3. 定义类的私有属性或方法:通过使用 Symbol,可以创建类的私有属性或方法,这些属性或方法不容易被外部访问或覆盖。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const _privateField = Symbol();

    class MyClass {
    constructor() {
    this[_privateField] = 'private value';
    }

    getPrivateValue() {
    return this[_privateField];
    }
    }

    const instance = new MyClass();
    console.log(instance.getPrivateValue()); // 输出: 'private value'
    console.log(instance[_privateField]); // undefined

从技术上说,symbol 不是 100% 隐藏的。有一个内建方法 Object.getOwnPropertySymbols(obj) 允许我们获取所有的 symbol。还有一个名为 Reflect.ownKeys(obj) 的方法可以返回一个对象的 所有 键,包括 symbol。但大多数库、内建方法和语法结构都没有使用这些方法。

对象迭代

JavaScript 迭代器

迭代器协议定义了如何从对象生成值序列。对象实现 next ()方法时成为迭代器。Next ()方法必须返回具有两个属性的对象:

  • value (the next value)
  • done (true or false)
value The value returned by the iterator (Can be omitted if done is true)
done true if the iterator has completed false if the iterator has produced a new value

A JavaScript iterable is an object that has a Symbol.iterator.

The Symbol.iterator is a function that returns a next() function.

An iterable can be iterated over with the code: for (const x of iterable) { }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Create an Object
myNumbers = {};

// Make it Iterable
myNumbers[Symbol.iterator] = function() {
let n = 0;
done = false;
return {
next() {
n += 10;
if (n == 100) {done = true}
return {value:n, done:done};
}
};
}

数组

以下语句创建了等效的数组:

1
2
3
const arr1 = new Array(element0, element1, /* … ,*/ elementN);
const arr2 = Array(element0, element1, /* … ,*/ elementN);
const arr3 = [element0, element1, /* … ,*/ elementN];

Length

length 属性是特殊的,你也可以给 length 属性赋值。写一个小于数组元素数量的值将截断数组,写 0 会彻底清空数组

稀疏数组

数组可以包含“空槽”,这与用值 undefined 填充的槽不一样。空槽可以通过以下方式之一创建:

JSCopy to Clipboard

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Array 构造函数:
const a = Array(5); // [ <5 empty items> ]

// 数组字面量中的连续逗号:
const b = [1, 2, , , 5]; // [ 1, 2, <2 empty items>, 5 ]

// 直接给大于 array.length 的索引设置值以形成空槽:
const c = [1, 2];
c[4] = 5; // [ 1, 2, <2 empty items>, 5 ]

// 通过直接设置 .length 拉长一个数组:
const d = [1, 2];
d.length = 5; // [ 1, 2, <3 empty items> ]

// 删除一个元素:
const e = [1, 2, 3, 4, 5];
delete e[2]; // [ 1, 2, <1 empty item>, 4, 5 ]

创建多维数组

数组是可以嵌套的,这就意味着一个数组可以作为一个元素被包含在另外一个数组里面。利用 JavaScript 数组的这个特性,可以创建多维数组。

以下代码创建了一个二维数组。

1
2
3
4
5
6
7
const a = new Array(4);
for (i = 0; i < 4; i++) {
a[i] = new Array(4);
for (j = 0; j < 4; j++) {
a[i][j] = "[" + i + "," + j + "]";
}
}

事件

事件是发生在你正在编程的系统中的事情——当事件发生时,系统产生(或“触发”)某种信号,并提供一种机制,当事件发生时,可以自动采取某种行动(即运行一些代码)。事件是在浏览器窗口内触发的,并倾向于附加到驻留在其中的特定项目。这可能是一个单一的元素,一组元素,当前标签中加载的 HTML 文档,或整个浏览器窗口。有许多不同类型的事件可以发生。

1
2
3
4
5
6
7
8
9
10
11
const btn = document.querySelector("button");

function random(number) {
return Math.floor(Math.random() * (number + 1));
}

btn.addEventListener("click", () => {
const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
document.body.style.backgroundColor = rndCol;
});

正如我们在上一个示例中所看到的,能够触发事件的对象有一个 addEventListener() 方法,这就是推荐的添加事件处理器的机制。

通过对 addEventListener() 的多次调用,每次提供不同的处理器,你可以为一个事件设置多个处理器:

JSCopy to Clipboard

1
2
myElement.addEventListener("click", functionA);
myElement.addEventListener("click", functionB);

事件处理器属性

可以触发事件的对象(如按钮)通常也有属性,其名称是 on,后面是事件的名称。例如,元素有一个属性 onclick。这被称为事件处理器属性。为了监听事件,你可以将处理函数分配给该属性。

例如,我们可以像这样重写随机颜色示例:

JSCopy to Clipboard

1
2
3
4
5
6
7
8
9
10
11
12
const btn = document.querySelector("button");

function random(number) {
return Math.floor(Math.random() * (number + 1));
}

function bgChange() {
const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
document.body.style.backgroundColor = rndCol;
}

btn.onclick = bgChange;

事件对象

有时候在事件处理函数内部,你可能会看到一个固定指定名称的参数,例如 eventevte。这被称为事件对象,它被自动传递给事件处理函数,以提供额外的功能和信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
const btn = document.querySelector("button");

function random(number) {
return Math.floor(Math.random() * (number + 1));
}

function bgChange(e) {
const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
e.target.style.backgroundColor = rndCol; //使用事件对象
console.log(e);
}

btn.addEventListener("click", bgChange);

事件冒泡

事件冒泡描述了浏览器如何处理针对嵌套元素的事件。

示例:

1
2
3
4
5
6
<body>
<div id="container">
<button>点我!</button>
</div>
<pre id="output"></pre>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15


const output = document.querySelector("#output");
function handleClick(e) {
output.textContent += `你在 ${e.currentTarget.tagName} 元素上进行了点击\n`;
}

const container = document.querySelector("#container");
const button = document.querySelector("button");

document.body.addEventListener("click", handleClick);
container.addEventListener("click", handleClick);
button.addEventListener("click", handleClick);


你会发现在用户单击按钮时,所有三个元素都触发了单击事件:

1
2
3
你在 BUTTON 元素上进行了点击
你在 DIV 元素上进行了点击
你在 BODY 元素上进行了点击

在这种情况下:

  • 最先触发按钮上的单击事件
  • 然后是按钮的父元素(<div> 元素)
  • 然后是 <div> 的父元素(<body> 元素)

使用 stopPropagation() 阻止冒泡行为

正如我们在上一节所看到的,事件冒泡有时会产生问题,但有一种方法可以防止这些问题。Event 对象有一个可用的函数,叫做 stopPropagation(),当在一个事件处理器中调用时,可以防止事件向任何其他元素传递。

1
2
3
4
5
6
7
8
9
10
11
12
const btn = document.querySelector("button");
const box = document.querySelector("div");
const video = document.querySelector("video");

btn.addEventListener("click", () => box.classList.remove("hidden"));

video.addEventListener("click", (event) => {
event.stopPropagation();
video.play();
});

box.addEventListener("click", () => box.classList.add("hidden"));

事件委托

我们看了一个由事件冒泡引起的问题以及如何解决它。不过,事件冒泡并不只是令人讨厌:它可以非常有用。特别是,它可以实现事件委托。在这种做法中,当我们想在用户与大量的子元素中的任何一个互动时运行一些代码时,我们在它们的父元素上设置事件监听器,让发生在它们身上的事件冒泡到它们的父元素上,而不必在每个子元素上单独设置事件监听器。

Playground | MDN (mozilla.org)

在这个例子中,我们使用 event.target 来获取事件的目标元素(也就是最里面的元素)。如果我们想访问处理这个事件的元素(在这个例子中是容器),我们可以使用 event.currentTarget

Javascript中的相等判断

  • ===——严格相等(三个等号)
  • ==——宽松相等(两个等号)
  • Object.is()

选择哪个运算取决于你需要什么样的比较。简单来说:

  • 在比较两个操作数时,双等号(==)将执行类型转换,并且会按照 IEEE 754 标准对 NaN-0+0 进行特殊处理(故 NaN != NaN,且 -0 == +0);
  • 三等号(===)做的比较与双等号相同(包括对 NaN-0+0 的特殊处理)但不进行类型转换;如果类型不同,则返回 false
  • Object.is() 既不进行类型转换,也不对 NaN-0+0 进行特殊处理(这使它和 === 在除了那些特殊数字值之外的情况具有相同的表现)。

JavaScript Best Practices

Avoid Global Variables

Minimize the use of global variables.

This includes all data types, objects, and functions.

Global variables and functions can be overwritten by other scripts.

Use local variables instead, and learn how to use closures.

Always Declare Local Variables

All variables used in a function should be declared as local variables.

Local variables must be declared with the var, the let, or the const keyword, otherwise they will become global variables.

Declarations on Top

It is a good coding practice to put all declarations at the top of each script or function.

This will:

  • Give cleaner code
  • Provide a single place to look for local variables
  • Make it easier to avoid unwanted (implied) global variables
  • Reduce the possibility of unwanted re-declarations

Initialize Variables

It is a good coding practice to initialize variables when you declare them.

This will:

  • Give cleaner code
  • Provide a single place to initialize variables
  • Avoid undefined values

Declare Objects with const

Declaring objects with const will prevent any accidental change of type:

1
2
const car = {type:"Fiat", model:"500", color:"white"};
car = "Fiat"; // Not possible

Declare Arrays with const

Declaring arrays with const will prevent any accidential change of type:

Don’t Use new Object()

  • Use "" instead of new String()
  • Use 0 instead of new Number()
  • Use false instead of new Boolean()
  • Use {} instead of new Object()
  • Use [] instead of new Array()
  • Use /()/ instead of new RegExp()
  • Use function (){} instead of new Function()

Beware of Automatic Type Conversions

JavaScript is loosely typed.

A variable can contain all data types.

A variable can change its data type:

Use === Comparison

The == comparison operator always converts (to matching types) before comparison.

The === operator forces comparison of values and type:

1
2
3
4
5
6
7
0 == "";        // true
1 == "1"; // true
1 == true; // true

0 === ""; // false
1 === "1"; // false
1 === true; // false

End Your Switches with Defaults

Always end your switch statements with a default. Even if you think there is no need for it.

Avoid Number, String, and Boolean as Objects

Always treat numbers, strings, or booleans as primitive values. Not as objects.

Declaring these types as objects, slows down execution speed, and produces nasty side effects:

1
2
3
let x = "John";             
let y = new String("John");
(x === y) // is false because x is a string and y is an object.

Avoid Using eval()

The eval() function is used to run text as code. In almost all cases, it should not be necessary to use it.

Because it allows arbitrary code to be run, it also represents a security problem.

JavaScript Performance

Reduce Activity in Loops

Loops are often used in programming.

Each statement in a loop, including the for statement, is executed for each iteration of the loop.

Statements or assignments that can be placed outside the loop will make the loop run faster.

1
2
3
4
5
6
//Bad:
for (let i = 0; i < arr.length; i++) {}

//Better Code:
let l = arr.length;
for (let i = 0; i < l; i++) {}

Reduce DOM Access

Accessing the HTML DOM is very slow, compared to other JavaScript statements.

If you expect to access a DOM element several times, access it once, and use it as a local variable:

1
2
const obj = document.getElementById("demo");
obj.innerHTML = "Hello";

Reduce DOM Size

Keep the number of elements in the HTML DOM small.

This will always improve page loading, and speed up rendering (page display), especially on smaller devices.

Every attempt to search the DOM (like getElementsByTagName) will benefit from a smaller DOM.

Delay JavaScript Loading

Putting your scripts at the bottom of the page body lets the browser load the page first.

While a script is downloading, the browser will not start any other downloads. In addition all parsing and rendering activity might be blocked.

The HTTP specification defines that browsers should not download more than two components in parallel.

An alternative is to use defer="true" in the script tag. The defer attribute specifies that the script should be executed after the page has finished parsing, but it only works for external scripts.

Promises

构造 Promise

1
2
3
4
5
6
7
8
9
10
11
12
let myPromise = new Promise(function(myResolve, myReject) {
// "Producing Code" (May take some time)

myResolve(); // when successful
myReject(); // when error
});

// "Consuming Code" (Must wait for a fulfilled Promise)
myPromise.then(
function(value) { /* code if successful */ },
function(error) { /* code if some error */ }
);

A JavaScript Promise object can be:

  • Pending
  • Fulfilled
  • Rejected

The Promise object supports two properties: state and result.

  • While a Promise object is “pending” (working), the result is undefined.

  • When a Promise object is “fulfilled”, the result is a value.

  • When a Promise object is “rejected”, the result is an error object.

Promise 的构造函数

Promise 构造函数是 JavaScript 中用于创建 Promise 对象的内置构造函数。

Promise 构造函数接受一个函数作为参数,该函数是同步的并且会被立即执行,所以我们称之为起始函数。起始函数包含两个参数 resolve 和 reject,分别表示 Promise 成功和失败的状态。起始函数执行成功时,它应该调用 resolve 函数并传递成功的结果。当起始函数执行失败时,它应该调用 reject 函数并传递失败的原因。

Promise 构造函数返回一个 Promise 对象,该对象具有以下几个方法:

  • then:用于处理 Promise 成功状态的回调函数。Then ()接受两个参数,一个是成功的回调,另一个是失败的回调。两者都是可选的,因此只能为成功或失败添加回调。
  • catch:用于处理 Promise 失败状态的回调函数。
  • finally:无论 Promise 是成功还是失败,都会执行的回调函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
new Promise(function (resolve, reject) {
console.log(1111);
resolve(2222);
}).then(function (value) {
console.log(value);
return 3333;
}).then(function (value) {
console.log(value);
throw "An error";
}).catch(function (err) {
console.log(err);
});

//执行结果:
1111
2222
3333

resolve() 中可以放置一个参数用于向下一个 then 传递一个值,then 中的函数也可以返回一个值传递给 then。但是,如果 then 中返回的是一个 Promise 对象,那么下一个 then 将相当于对这个返回的 Promise 进行操。reject() 参数中一般会传递一个异常给之后的 catch 函数用于处理异常。

但是请注意以下两点:

  • resolve 和 reject 的作用域只有起始函数,不包括 then 以及其他序列;
  • resolve 和 reject 并不能够使起始函数停止运行,别忘了 return。

Async/Await

异步函数(async function)是 ECMAScript 2017 (ECMA-262) 标准的规范,几乎被所有浏览器所支持,除了 Internet Explorer。

在 Promise 中我们编写过一个 Promise 函数:

1
2
3
4
5
6
7
8
function print(delay, message) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log(message);
resolve();
}, delay);
});
}

我们可以将这段代码变得更好看:

1
2
3
4
5
6
async function asyncFunc() {
await print(1000, "First");
await print(4000, "Second");
await print(3000, "Third");
}
asyncFunc();

异步函数实际上原理与 Promise 原生 API 的机制是一模一样的,只不过更便于程序员阅读。

异步函数 async function 中可以使用 await 指令,await 关键字只能在异步函数 async function 中使用。await 指令后必须跟着一个 Promise,异步函数会在这个 Promise 运行中暂停,直到其运行结束再继续运行。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async function getFile() {
let myPromise = new Promise(function(resolve) {
let req = new XMLHttpRequest();
req.open('GET', "mycar.html");
req.onload = function() {
if (req.status == 200) {
resolve(req.response);
} else {
resolve("File not Found");
}
};
req.send();
});
document.getElementById("demo").innerHTML = await myPromise;
}

getFile();