《JavaScript高级程序设计(第四版)》(更新中,更新到第3章…)

好像一直以来都没有从头到尾看过一本技术书籍,这次想试试,顺便记录一下阅读过程中的所见所思。。。

1.什么是 JavaScript

JavaScript 是一门用来与网页交互的脚本语言,包含以下三个组成部分。

  • ECMAScript:由 ECMA-262 定义并提供核心功能。

  • 文档对象模型(DOM):提供与网页内容交互的方法和接口。

  • 浏览器对象模型(BOM):提供与浏览器交互的方法和接口。

JavaScript 的这三个部分得到了五大 Web 浏览器(IE、Firefox、Chrome、Safari 和 Opera)不同程度的支持。所有浏览器基本上对 ES5(ECMAScript 5)提供了完善的支持,而对 ES6(ECMAScript 6)和ES7(ECMAScript 7)的支持度也在不断提升。这些浏览器对 DOM 的支持各不相同,但对 Level 3 的支持日益趋于规范。HTML5 中收录的 BOM 会因浏览器而异,不过开发者仍然可以假定存在很大一部分公共特性。

2.HTML 中的 JavaScript

2.1 <script>元素

在 HTML 页面中插入 JavaScript 的主要方法有两种:

  1. 在 HTML 中使用
  2. 外部引用 JavaScript 脚本

<script>元素有下列 8 个属性:

属性 描述
async 表示应该立即开始下载脚本,但不能阻止其他页面动作,比如下载资源或等待其他脚本加载。只对外部脚本文件有效
charset 使用 src 属性指定的代码字符集。这个属性很少使用,因为大多数浏览器不在乎它的值
crossorigin 配置相关请求的CORS(跨源资源共享)设置。默认不使用CORS。crossorigin= “anonymous”配置文件请求不必设置凭据标志。crossorigin=”use-credentials”设置凭据标志,意味着出站请求会包含凭据
defer 表示脚本可以延迟到文档完全被解析和显示之后再执行。只对外部脚本文件有效。在 IE7 及更早的版本中,对行内脚本也可以指定这个属性
integrity 允许比对接收到的资源和指定的加密签名以验证子资源完整性(SRI,Subresource Integrity)。如果接收到的资源的签名与这个属性指定的签名不匹配,则页面会报错,脚本不会执行。这个属性可以用于确保内容分发网络(CDN,Content Delivery Network)不会提供恶意内容
language 废弃。最初用于表示代码块中的脚本语言(如”JavaScript”、”JavaScript 1.2″或”VBScript”)。大多数浏览器都会忽略这个属性,不应该再使用它
src 表示包含要执行的代码的外部文件
type 代替 language,表示代码块中脚本语言的内容类型(也称 MIME 类型)。按照惯例,这个值始终都是”text/javascript”,尽管”text/javascript”和”text/ecmascript”都已经废弃了。JavaScript 文件的 MIME 类型通常是”application/x-javascript”,不过给type 属性这个值有可能导致脚本被忽略。在非 IE 的浏览器中有效的其他值还有”application/javascript”和”application/ecmascript”。如果这个值是 module,则代码会被当成 ES6 模块,而且只有这时候代码中才能出现 import 和 export 关键字

注意:

deferasync都是用于异步加载JavaScript脚本的属性,但它们之间有一些区别:

  1. 加载顺序:使用defer属性加载的脚本会在HTML文档解析完成后按照顺序依次执行,而使用async属性加载的脚本则不保证执行的顺序,只要下载完成就会立即执行。

  2. 执行时机:使用defer属性加载的脚本会在文档解析完成后、DOMContentLoaded事件之前执行,而使用async属性加载的脚本则会在下载完成后立即执行,不会等待文档解析完成。

  3. 对DOM的影响:使用defer属性加载的脚本不会阻塞DOM的解析和渲染,也不会影响页面的加载速度,因为它会等到HTML文档解析完成后再执行。而使用async属性加载的脚本可能会影响DOM的解析和渲染,因为它会在下载完成后立即执行,如果脚本需要操作DOM元素,可能会影响到页面的加载速度和用户体验。

综上所述,使用defer属性加载的脚本适用于需要按顺序执行的脚本,并且不依赖于DOM元素的脚本;而使用async属性加载的脚本适用于独立的、不依赖于其他脚本和DOM元素的脚本。

2.2 行内代码与外部文件

推荐使用外部文件的理由如下。

  • 可维护性。JavaScript 代码如果分散到很多 HTML 页面,会导致维护困难。而用一个目录保存所有 JavaScript 文件,则更容易维护,这样开发者就可以独立于使用它们的 HTML 页面来编辑代码。

  • 缓存。浏览器会根据特定的设置缓存所有外部链接的 JavaScript 文件,这意味着如果两个页面都用到同一个文件,则该文件只需下载一次。这最终意味着页面加载更快。

  • 适应未来。通过把 JavaScript 放到外部文件中,就不必考虑用 XHTML 或前面提到的注释黑科技。包含外部 JavaScript 文件的语法在 HTML 和 XHTML 中是一样的。在配置浏览器请求外部文件时,要重点考虑的一点是它们会占用多少带宽。在 SPDY/HTTP2 中,预请求的消耗已显著降低,以轻量、独立 JavaScript 组件形式向客户端送达脚本更具优势。

2.3 文档模式

IE5.5 发明了文档模式的概念,即可以使用 doctype 切换文档模式。最初的文档模式有两种:混杂模式(quirks mode)和标准模式(standards mode)。前者让 IE 像 IE5 一样(支持一些非标准的特性),后者让 IE 具有兼容标准的行为。虽然这两种模式的主要区别只体现在通过 CSS 渲染的内容方面,但对JavaScript 也有一些关联影响,或称为副作用。本书会经常提到这些副作用。

  1. 混杂模式下的盒模型为IE的盒模型
IE标准下的盒模型的宽高分别为:
CSS中的宽(width)= 内容(content)的宽 +(border+padding)*2
CSS中的高(height)= 内容(content)的高 +(border+padding)*2
  1. 标准模式下的盒模型是W3C标准的盒模型
在w3c标准中,盒模型的宽高分别为:
CSS中的宽(width)= 内容(content)的宽
CSS中的高(height)= 内容(content)的高

在标准模式下,所有尺寸都必须包含单位,若不含单位的话,会被忽略;

在混杂模式下,可以把 style.width设置为”20″,相当于”20px”。

2.4 <noscript>元素

  • 浏览器不支持脚本;

  • 浏览器对脚本的支持被关闭。

<noscript> 
 <p>This page requires a JavaScript-enabled browser.</p> 
</noscript>

这个例子是在脚本不可用时让浏览器显示一段话。如果浏览器支持脚本,则用户永远不会看到它。

2.5 小结

JavaScript 是通过<script>元素插入到 HTML 页面中的。这个元素可用于把 JavaScript 代码嵌入到HTML 页面中,跟其他标记混合在一起,也可用于引入保存在外部文件中的 JavaScript。本章的重点可以总结如下。

  • 要包含外部 JavaScript 文件,必须将 src 属性设置为要包含文件的 URL。文件可以跟网页在同一台服务器上,也可以位于完全不同的域。

  • 所有<script>元素会依照它们在网页中出现的次序被解释。在不使用 defer 和 async 属性的情况下,包含在<script>元素中的代码必须严格按次序解释。

  • 对不推迟执行的脚本,浏览器必须解释完位于<script>元素中的代码,然后才能继续渲染页面的剩余部分。为此,通常应该把<script>元素放到页面末尾,介于主内容之后及</body>标签之前。

  • 可以使用 defer 属性把脚本推迟到文档渲染完毕后再执行。推迟的脚本原则上按照它们被列出的次序执行。

  • 可以使用 async 属性表示脚本不需要等待其他脚本,同时也不阻塞文档渲染,即异步加载。异步脚本不能保证按照它们在页面中出现的次序执行。

  • 通过使用<noscript>元素,可以指定在浏览器不支持脚本时显示的内容。如果浏览器支持并启用脚本,则<noscript>元素中的任何内容都不会被渲染。

3.语言基础

任何语言的核心所描述的都是这门语言在最基本的层面上如何工作,涉及语法、操作符、数据类型以及内置功能,在此基础之上才可以构建复杂的解决方案。如前所述,ECMA-262以一个名为ECMAScript的伪语言的形式,定义了 JavaScript 的所有这些方面。

3.1语法

3.1.1 区分大小写

ECMAScript 中一切都区分大小写。无论是变量、函数名还是操作符,都区分大小写。

3.1.2 标识符

所谓标识符,就是变量、函数、属性或函数参数的名称。标识符可以由一或多个下列字符组成:

  • 第一个字符必须是一个字母、下划线(_)或美元符号($);
  • 剩下的其他字符可以是字母、下划线、美元符号或数字。

按照惯例,ECMAScript 标识符使用驼峰大小写形式,即第一个单词的首字母小写,后面每个单词的首字母大写,如:

  • firstWord
  • myCar
  • doSomethingImportant

注意:关键字、保留字、true、false 和 null 不能作为标识符

3.1.3 注释

ECMAScript 采用 C 语言风格的注释,包括单行注释和块注释。

// 单行注释

/* 这是多行注释 */

3.1.4 严格模式

要对整个脚本启用严格模式,在脚本开头加上这一行:

"use strict";

虽然看起来像个没有赋值给任何变量的字符串,但它其实是一个预处理指令。任何支持的 JavaScript引擎看到它都会切换到严格模式。选择这种语法形式的目的是不破坏 ECMAScript 3 语法。也可以单独指定一个函数在严格模式下执行,只要把这个预处理指令放到函数体开头即可:

function doSomething() {
  "use strict";
   // 函数体
}

严格模式会影响 JavaScript 执行的很多方面,因此本书在用到它时会明确指出来。所有现代浏览器都支持严格模式。

3.1.5 语句

ECMAScript 中的语句以分号结尾。省略分号意味着由解析器确定语句在哪里结尾,如下面的例子所示,加分号也有助于在某些情况下提升性能,因为解析器会尝试在合适的位置补上分号以纠正语法错误。

let sum = a + b // 没有分号也有效,但不推荐
let diff = a - b; // 加分号有效,推荐

if 之类的控制语句只在执行多条语句时要求必须有代码块。不过,最佳实践是始终在控制语句中使用代码块,即使要执行的只有一条语句,如下例所示:

// 有效,但容易导致错误,应该避免
if (test) 
    console.log(test);

// 推荐
if (test) {
    console.log(test);
}

在控制语句中使用代码块可以让内容更清晰,在需要修改代码时也可以减少出错的可能性。

3.2 关键字与保留字

ECMA-262 描述了一组保留的关键字,这些关键字有特殊用途,比如表示控制语句的开始和结束,或者执行特定的操作。按照规定,保留的关键字不能用作标识符或属性名。ECMA-262 第 6 版规定的所有关键字如下:

break do in typeof case else instanceof var

catch export new void class extends return while

const finally super with continue for switch yield

debugger function this default if throw delete import try

规范中也描述了一组未来的保留字,同样不能用作标识符或属性名。虽然保留字在语言中没有特定用途,但它们是保留给将来做关键字用的。以下是 ECMA-262 第 6 版为将来保留的所有词汇:

//始终保留:
enum

//严格模式下保留:
implements package public interface protected static let private

//模块代码中保留:
await

3.3 变量

ECMAScript 变量是松散类型的,意思是变量可以用于保存任何类型的数据。每个变量只不过是一个用于保存任意值的命名占位符。有 3 个关键字可以声明变量:var、const 和 let。其中,var 在 ECMAScript 的所有版本中都可以使用,而 const 和 let 只能在 ECMAScript 6 及更晚的版本中使用。

想知道三者的区别,看一下这篇文章即可解读let-const-var的区别

3.3.1 var 关键字

var message;
var message = "hi";
message = 100; // 合法,但不推荐

var message = "hi",
    found = false,
    age = 29;

var name;
var name;
  1. var 声明作用域
function test() {
    var message = "hi"; // 局部变量
}
test();
console.log(message); // 出错!
function test() {
    message = "hi"; // 全局变量
}
test();
console.log(message); // "hi"
  1. var 声明提升
    使用 var 时,下面的代码不会报错。这是因为使用这个关键字声明的变量会自动提升到函数作用域顶部:
function foo() {
    console.log(age);
    var age = 26;
}
foo(); // undefined

等价下面

function foo() {
    var age;
    console.log(age);
    age = 26;
}
foo(); // undefined

3.3.2 let 声明

let 跟 var 的作用差不多,但有着非常重要的区别。最明显的区别是,let 声明的范围是块作用域,而 var 声明的范围是函数作用域。

if (true) {
    var name = 'Matt';
    console.log(name); // Matt
}
console.log(name); // Matt
    
if (true) {
    let age = 26;
    console.log(age); // 26
}
console.log(age); // ReferenceError: age 没有定义
    let age;
    let age; // SyntaxError;标识符 age 已经声明过了
var name;
let name; // SyntaxError

let age;
var age; // SyntaxError
  1. 暂时性死区
// name 会被提升
console.log(name); // undefined
var name = 'Matt';

// age 不会被提升
console.log(age); // ReferenceError:age 没有定义
let age = 26;
    
在解析代码时,JavaScript 引擎也会注意出现在块后面的 let 声明,只不过在此之前不能以任何方式来引用未声明的变量。
在 let 声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),
在此阶段引用任何后面才声明的变量都会抛出 ReferenceError
  1. 全局声明

与 var 关键字不同,使用 let 在全局作用域中声明的变量不会成为 window 对象的属性(var 声明的变量则会)。

var name = 'Matt';
console.log(window.name); // 'Matt'

let age = 26;
console.log(window.age); // undefined
  1. 条件声明

在使用 var 声明变量时,由于声明会被提升,JavaScript 引擎会自动将多余的声明在作用域顶部合并为一个声明。因为 let 的作用域是块,所以不可能检查前面是否已经使用 let 声明过同名变量,同时也就不可能在没有声明的情况下声明它。

  1. for 循环中的 let 声明
for (var i = 0; i < 5; ++i) {
    // 循环逻辑
}
console.log(i); // 5

for (let i = 0; i < 5; ++i) {
    // 循环逻辑
}
console.log(i); // ReferenceError: i 没有定义         
for (var i = 0; i < 5; ++i) {
    setTimeout(() => console.log(i), 0)
}
// 你可能以为会输出 0、1、2、3、4
// 实际上会输出 5、5、5、5、5

for (let i = 0; i < 5; ++i) {
    setTimeout(() => console.log(i), 0)
}
// 会输出 0、1、2、3、4

3.3.3 const 声明

const 的行为与 let 基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改 const 声明的变量会导致运行时错误。

const age = 26;
age = 36; // TypeError: 给常量赋值

// const 也不允许重复声明
const name = 'Matt';
const name = 'Nicholas'; // SyntaxError

// const 声明的作用域也是块
const name = 'Matt';
if (true) {
    const name = 'Nicholas';
}
console.log(name); // Matt

const 声明的限制只适用于它指向的变量的引用。换句话说,如果 const 变量引用的是一个对象,那么修改这个对象内部的属性并不违反 const 的限制。

const person = {};
person.name = 'Matt'; // ok

3.3.4 声明风格及最佳实践

  1. 不使用 var
  2. const 优先,let 次之

3.4数据类型

ECMAScript 有 6 种简单数据类型(也称为原始类型):Undefined、Null、Boolean、Number、String 和 Symbol。Symbol(符号)是 ECMAScript 6 新增的。还有一种复杂数据类型叫 Object(对象)。

想知道更详细的数据类型,看一下这篇文章即可数据类型

3.4.1 typeof 操作符

let message = "some string";
console.log(typeof message); // "string"

注意:调用typeof null 返回的是”object”

3.4.2 Undefined 类型

let message;
console.log(message == undefined); // true

3.4.3 Null 类型

let car = null;
console.log(typeof car); // "object"    

3.4.4 Boolean 类型

let found = true;
let lost = false;
数据类型 转换为 true 的值 转换为 false 的值
Boolean true false
String 非空字符串 “”(空字符串)
Number 非零数值(包括无穷值 0、NaN
Object 任意对象
Undefined undefined

3.4.5 Number 类型

let intNum = 55; // 整数
    
let octalNum1 = 070; // 八进制的 56
let octalNum2 = 079; // 无效的八进制值,当成 79 处理
let octalNum3 = 08; // 无效的八进制值,当成 8 处理
    
let hexNum1 = 0xA; // 十六进制 10
let hexNum2 = 0x1f; // 十六进制 31

let floatNum1 = 1.1;
let floatNum2 = 0.1;
let floatNum3 = .1; // 有效,但不推荐

let floatNum = 3.125e7; // 等于 31250000

let result = Number.MAX_VALUE + Number.MAX_VALUE;
console.log(isFinite(result)); // false

console.log(0/0); // NaN
console.log(-0/+0); // NaN
    
console.log(isNaN(NaN)); // true
console.log(isNaN(10)); // false,10 是数值
console.log(isNaN("10")); // false,可以转换为数值 10
console.log(isNaN("blue")); // true,不可以转换为数值
console.log(isNaN(true)); // false,可以转换为数值 1

console.log(5/0); // Infinity
console.log(5/-0); // -Infinity

注意:实际中可能存在正零(+0)和负零(-0)。正零和负零在所有情况下都被认为是等同的,使用Number.NEGATIVE_INFINITY 和 Number.POSITIVE_INFINITY 也可以获取正、负 Infinity。

数值转换

有 3 个函数可以将非数值转换为数值:Number()、parseInt()和 parseFloat()。Number()是转型函数,可用于任何数据类型。后两个函数主要用于将字符串转换为数值。对于同样的参数,这 3 个函数执行的操作也不同

Number()函数基于如下规则执行转换。

  • 布尔值,true 转换为 1,false 转换为 0。
  • 数值,直接返回。
  • null,返回 0。
  • undefined,返回 NaN。
  • 字符串,应用以下规则。
    • 如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值。因此, Number(“1”)返回 1,Number(“123”)返回 123,Number(“011”)返回 11(忽略前面的零)。
    • 如果字符串包含有效的浮点值格式如”1.1″,则会转换为相应的浮点值(同样,忽略前面的零)。
    • 如果字符串包含有效的十六进制格式如”0xf”,则会转换为与该十六进制值对应的十进制整数值。
    • 如果是空字符串(不包含字符),则返回 0。
    • 如果字符串包含除上述情况之外的其他字符,则返回 NaN。
  • 对象,调用 valueOf()方法,并按照上述规则转换返回的值。如果转换结果是 NaN,则调用toString()方法,再按照转换字符串的规则转换。
let num1 = Number("Hello world!"); // NaN
let num2 = Number(""); // 0
let num3 = Number("000011"); // 11
let num4 = Number(true); // 1

parseInt()函数更专注于字符串是否包含数值模式

let num1 = parseInt("1234blue"); // 1234
let num2 = parseInt(""); // NaN
let num3 = parseInt("0xA"); // 10,解释为十六进制整数
let num4 = parseInt(22.5); // 22
let num5 = parseInt("70"); // 70,解释为十进制值
let num6 = parseInt("0xf"); // 15,解释为十六进制整数

parseInt()也接收第二个参数,用于指定底数(进制数)。

let num = parseInt("0xAF", 16); // 175

//事实上,如果提供了十六进制参数,那么字符串前面的"0x"可以省掉:
let num1 = parseInt("AF", 16); // 175
let num2 = parseInt("AF"); // NaN

parseFloat()只解析十进制值,因此不能指定底数

let num1 = parseFloat("1234blue"); // 1234,按整数解析
let num2 = parseFloat("0xA"); // 0
let num3 = parseFloat("22.5"); // 22.5
let num4 = parseFloat("22.34.5"); // 22.34
let num5 = parseFloat("0908.5"); // 908.5
let num6 = parseFloat("3.125e7"); // 31250000

3.4.6 String 类型

let firstName = "John";
let lastName = 'Jacob';
let lastName = `Jingleheimerschmidt`

字符字面量

字 面 量 含 义
\n 换行
\t 制表
\b 退格
\r 回车
\f 换页
\ 反斜杠(\)
单引号(’),在字符串以单引号标示时使用,例如’He said, ‘hey.”
双引号(”),在字符串以双引号标示时使用,例如”He said, “hey.””
` 反引号(`),在字符串以反引号标示时使用,例如`He said, `hey.“
\xnn 以十六进制编码 nn 表示的字符(其中 n 是十六进制数字 0~F),例如\x41 等于”A”
\unnnn 以十六进制编码 nnnn 表示的 Unicode 字符(其中 n 是十六进制数字 0~F),例如\u03a3 等于希腊字符”Σ”

toString()方法可见于数值、布尔值、对象和字符串值。(没错,字符串值也有 toString()方法,该方法只是简单地返回自身的一个副本。)null 和 undefined 值没有 toString()方法。

let age = 11;
let ageAsString = age.toString(); // 字符串"11"

let found = true;
let foundAsString = found.toString(); // 字符串"true"

如果你不确定一个值是不是 null 或 undefined,可以使用 String()转型函数,它始终会返回表示相应类型值的字符串。String()函数遵循如下规则。

  • 如果值有 toString()方法,则调用该方法(不传参数)并返回结果。
  • 如果值是 null,返回”null”。
  • 如果值是 undefined,返回”undefined”
let value1 = 10;
let value2 = true;
let value3 = null;
let value4;

console.log(String(value1)); // "10"
console.log(String(value2)); // "true"
console.log(String(value3)); // "null"
console.log(String(value4)); // "undefined"

注意:用加号操作符给一个值加上一个空字符串””也可以将其转换为字符串

模板字面量标签函数

模板字面量也支持定义标签函数(tag function),而通过标签函数可以自定义插值行为。标签函数会接收被插值记号分隔后的模板和对每个表达式求值的结果。

辅助理解1

辅助理解2

let a = 6;
let b = 9;

function simpleTag(strings, aValExpression, bValExpression, sumExpression) {
    console.log(strings);
    console.log(aValExpression);
    console.log(bValExpression);
    console.log(sumExpression);
    return 'foobar';
}

let untaggedResult = `${ a } + ${ b } = ${ a + b }`;
let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`;

// ["", " + ", " = ", ""]
// 6
// 9
// 15

console.log(untaggedResult); // "6 + 9 = 15"
console.log(taggedResult); // "foobar"

原始字符串

使用模板字面量也可以直接获取原始的模板字面量内容(如换行符或 Unicode 字符),而不是被转换后的字符表示。为此,可以使用默认的 String.raw 标签函数:

// Unicode 示例
// \u00A9 是版权符号
console.log(`\u00A9`); // ©
console.log(String.raw`\u00A9`); // \u00A9

// 换行符示例
console.log(`first line\nsecond line`);
// first line
// second line

console.log(String.raw`first line\nsecond line`); // "first line\nsecond line"

也可以通过标签函数的第一个参数,即字符串数组的.raw 属性取得每个字符串的原始内容:

function printRaw(strings) {
    console.log('Actual characters:');
    for (const string of strings) {
        console.log(string);
    }
    console.log('Escaped characters;');

    for (const rawString of strings.raw) {
        console.log(rawString);
    }
}

printRaw`\u00A9${ 'and' }\n`;

// Actual characters:
// ©
//(换行符)
// Escaped characters:
// \u00A9
// \n

3.4.7 Symbol 类型

symbol 是一种基本数据类型

let sym = Symbol();
console.log(typeof sym); // symbol

let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(genericSymbol == otherGenericSymbol); // false
console.log(fooSymbol == otherFooSymbol); // false

let fooSymbol = Symbol('foo');
console.log(fooSymbol); // Symbol(foo);

let mySymbol = new Symbol(); // TypeError: Symbol is not a constructor
let mySymbol = Symbol();
let myWrappedSymbol = Object(mySymbol);
console.log(typeof myWrappedSymbol); // "object"

使用全局符号注册表

let fooGlobalSymbol = Symbol.for('foo');
console.log(typeof fooGlobalSymbol); // symbol

let fooGlobalSymbol = Symbol.for('foo'); // 创建新符号
let otherFooGlobalSymbol = Symbol.for('foo'); // 重用已有符号
console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true

//采用相同的符号描述,在全局注册表中定义的符号跟使用 Symbol()定义的符号也并不等同:
let localSymbol = Symbol('foo');
let globalSymbol = Symbol.for('foo');
console.log(localSymbol === globalSymbol); // false

Symbol.keyFor()来查询全局注册表,这个方法接收符号,返回该全局符号对应的字符串键。如果查询的不是全局符号,则返回 undefined。

// 创建全局符号
let s = Symbol.for('foo');
console.log(Symbol.keyFor(s)); // foo

// 创建普通符号
let s2 = Symbol('bar');
console.log(Symbol.keyFor(s2)); // undefined

// 如果传给 Symbol.keyFor()的不是符号,则该方法抛出 TypeError:
Symbol.keyFor(123); // TypeError: 123 is not a symbol

使用符号作为属性

let s1 = Symbol('foo'),
    s2 = Symbol('bar'),
    s3 = Symbol('baz'),
    s4 = Symbol('qux');

let o = {
    [s1]: 'foo val'
};
// 这样也可以:o[s1] = 'foo val';
console.log(o); // {Symbol(foo): foo val}

Object.defineProperty(o, s2, {value: 'bar val'});
console.log(o); // {Symbol(foo): foo val, Symbol(bar): bar val}

Object.defineProperties(o, {
    [s3]: {value: 'baz val'},
    [s4]: {value: 'qux val'}
});
console.log(o);
// {Symbol(foo): foo val, Symbol(bar): bar val,
// Symbol(baz): baz val, Symbol(qux): qux val}

常用内置符号

详细可参考

注意:在提到 ECMAScript 规范时,经常会引用符号在规范中的名称,前缀为@@。比如,@@iterator 指的就是Symbol.iterator

  1. Symbol.iterator
    • 这个符号作为一个属性表示“一个方法,该方法返回对象默认的迭代器。由 for-of 语句使用”。换句话说,这个符号表示实现迭代器 API 的函数。
  2. Symbol.asyncIterator
    • 自定义对象上重新定义Symbol.iterator 的值,来改变 for-await-of 在迭代该对象时的行为
  3. Symbol.hasInstance
    • 这个符号作为一个属性表示“一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由 instanceof 操作符使用”
  4. Symbol.isConcatSpreadable
    • 这个符号作为一个属性表示“一个布尔值,如果是 true,则意味着对象应该用 Array.prototype.concat()打平其数组元素”
  5. Symbol.match
    • 这个符号作为一个属性表示“一个正则表达式方法,该方法用正则表达式去匹配字符串。由 String.prototype.match()方法使用”
  6. Symbol.replace
    • 这个符号作为一个属性表示“一个正则表达式方法,该方法替换一个字符串中匹配的子串。由 String.prototype.replace()方法使用”
  7. Symbol.search
    • 这个符号作为一个属性表示“一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引。由 String.prototype.search()方法使用”
  8. Symbol.species
    • 这个符号作为一个属性表示“一个函数值,该函数作为创建派生对象的构造函数”。这个属性在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方法。用 Symbol.species 定义静态的获取器(getter)方法,可以覆盖新创建实例的原型定义
  9. Symbol.split
    • 这个符号作为一个属性表示“一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由 String.prototype.split()方法使用”
  10. Symbol.toPrimitive
    • 这个符号作为一个属性表示“一个方法,该方法将对象转换为相应的原始
      值。由 ToPrimitive 抽象操作使用”。很多内置操作都会尝试强制将对象转换为原始值,包括字符串、数值和未指定的原始类型。对于一个自定义对象实例,通过在这个实例的 Symbol.toPrimitive 属性上定义一个函数可以改变默认行为
  11. Symbol.toStringTag
    • 这个符号作为一个属性表示“一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法 Object.prototype.toString()使用”
  12. Symbol.unscopables
    • 这个符号作为一个属性表示“一个对象,该对象所有的以及继承的属性,都会从关联对象的 with 环境绑定中排除”。设置这个符号并让其映射对应属性的键值为 true,就可以阻止该属性出现在 with 环境绑定中

3.4.8 Object 类型

let o = new Object();
let o = new Object; // 合法,但不推荐

每个 Object 实例都有如下属性和方法。

  • constructor:用于创建当前对象的函数。在前面的例子中,这个属性的值就是 Object()函数。
  • hasOwnProperty(propertyName):用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查的属性名必须是字符串(如 o.hasOwnProperty(“name”))或符号。
  • isPrototypeOf(object):用于判断当前对象是否为另一个对象的原型。(第 8 章将详细介绍原型。)
  • propertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用(本章稍后讨论的)for-in 语句枚举。与 hasOwnProperty()一样,属性名必须是字符串。
  • toLocaleString():返回对象的字符串表示,该字符串反映对象所在的本地化执行环境。
  • toString():返回对象的字符串表示。
  • valueOf():返回对象对应的字符串、数值或布尔值表示。通常与 toString()的返回值相同。

3.5 操作符

操作符,包括数学操作符(如加、减)、位操作符、关系操作符和相等操作符等。ECMAScript 中的操作符是独特的,因为它们可用于各种值,包括字符串、数值、布尔值,甚至还有对象。在应用给对象时,操作符通常会调用 valueOf()和/或 toString()方法来取得可以计算的值。

3.5.1 一元操作符

  1. 递增/递减操作符
  2. 一元加和减

3.5.2 位操作符

  1. 按位非
let num1 = 25; // 二进制 00000000000000000000000000011001
let num2 = ~num1; // 二进制 11111111111111111111111111100110
console.log(num2); // -26
  1. 按位与

按位与操作在两个位都是 1 时返回 1,在任何一位是 0 时返回 0。

第一个数值的位 第二个数值的位 结 果
1 1 1
1 0 0
0 1 0
0 0 0
let result = 25 & 3;
console.log(result); // 1

25 = 0000 0000 0000 0000 0000 0000 0001 1001
 3 = 0000 0000 0000 0000 0000 0000 0000 0011
---------------------------------------------
AND = 0000 0000 0000 0000 0000 0000 0000 0001
  1. 按位或

按位或操作在至少一位是 1 时返回 1,两位都是 0 时返回 0

第一个数值的位 第二个数值的位 结 果
1 1 1
1 0 1
0 1 1
0 0 0
let result = 25 | 3;
console.log(result); // 27

25 = 0000 0000 0000 0000 0000 0000 0001 1001
 3 = 0000 0000 0000 0000 0000 0000 0000 0011
---------------------------------------------
OR = 0000 0000 0000 0000 0000 0000 0001 1011
  1. 按位异或

按位异或与按位或的区别是,它只在一位上是 1 的时候返回 1(两位都是 1 或 0,则返回 0)

第一个数值的位 第二个数值的位 结 果
1 1 0
1 0 1
0 1 1
0 0 0
let result = 25 ^ 3;
console.log(result); // 26

25 = 0000 0000 0000 0000 0000 0000 0001 1001
 3 = 0000 0000 0000 0000 0000 0000 0000 0011
---------------------------------------------
XOR = 0000 0000 0000 0000 0000 0000 0001 1010
  1. 左移

左移操作符用两个小于号(<<)表示,会按照指定的位数将数值的所有位向左移动

let oldValue = 2; // 等于二进制 10
let newValue = oldValue << 5; // 等于二进制 1000000,即十进制 64

《JavaScript高级程序设计(第四版)》(更新中,更新到第3章...)

  1. 有符号右移

有符号右移由两个大于号(>>)表示,会将数值的所有 32 位都向右移,同时保留符号(正或负)。有符号右移实际上是左移的逆运算。比如,如果将 64 右移 5 位,那就是 2:

let oldValue = 64; // 等于二进制 1000000
let newValue = oldValue >> 5; // 等于二进制 10,即十进制 2

《JavaScript高级程序设计(第四版)》(更新中,更新到第3章...)

  1. 无符号右移

无符号右移用 3 个大于号表示(>>>),会将数值的所有 32 位都向右移。对于正数,无符号右移与有符号右移结果相同。仍然以前面有符号右移的例子为例,64 向右移动 5 位,会变成 2:

let oldValue = 64; // 等于二进制 1000000
let newValue = oldValue >>> 5; // 等于二进制 10,即十进制 2

let oldValue = -64; // 等于二进制 11111111111111111111111111000000
let newValue = oldValue >>> 5; // 等于十进制 134217726

3.5.3 布尔操作符

  1. 逻辑非 !
  2. 逻辑与 &&
  3. 逻辑或 ||

3.5.4 乘性操作符

  1. 乘法操作符
    • 如果操作数都是数值,则执行常规的乘法运算,即两个正值相乘是正值,两个负值相乘也是正值,正负符号不同的值相乘得到负值。如果 ECMAScript 不能表示乘积,则返回 Infinity 或-Infinity。
    • 如果有任一操作数是 NaN,则返回 NaN。
    • 如果是 Infinity 乘以 0,则返回 NaN。
    • 如果是 Infinity 乘以非 0的有限数值,则根据第二个操作数的符号返回 Infinity 或-Infinity。
    • 如果是 Infinity 乘以 Infinity,则返回 Infinity。
    • 如果有不是数值的操作数,则先在后台用 Number()将其转换为数值,然后再应用上述规则。
let result = 34 * 56;
  1. 除法操作符
    • 如果操作数都是数值,则执行常规的除法运算,即两个正值相除是正值,两个负值相除也是正值,符号不同的值相除得到负值。如果ECMAScript不能表示商,则返回Infinity或-Infinity。
    • 如果有任一操作数是 NaN,则返回 NaN。
    • 如果是 Infinity 除以 Infinity,则返回 NaN。
    • 如果是 0 除以 0,则返回 NaN。
    • 如果是非 0 的有限值除以 0,则根据第一个操作数的符号返回 Infinity 或-Infinity。
    • 如果是 Infinity 除以任何数值,则根据第二个操作数的符号返回 Infinity 或-Infinity。
    • 如果有不是数值的操作数,则先在后台用 Number()函数将其转换为数值,然后再应用上述规则。
let result = 66 / 11;
  1. 取模操作符
    • 如果操作数是数值,则执行常规除法运算,返回余数。
    • 如果被除数是无限值,除数是有限值,则返回 NaN。
    • 如果被除数是有限值,除数是 0,则返回 NaN。
    • 如果是 Infinity 除以 Infinity,则返回 NaN。
    • 如果被除数是有限值,除数是无限值,则返回被除数。
    • 如果被除数是 0,除数不是 0,则返回 0。
    • 如果有不是数值的操作数,则先在后台用 Number()函数将其转换为数值,然后再应用上述规则。
let result = 26 % 5; // 等于 1

3.5.5 指数操作符

console.log(Math.pow(3, 2); // 9
console.log(3 ** 2); // 9
    
console.log(Math.pow(16, 0.5); // 4
console.log(16** 0.5); // 4

let squared = 3;
squared **= 2;
console.log(squared); // 9

let sqrt = 16;
sqrt **= 0.5;
console.log(sqrt); // 4

3.5.6 加性操作符

  1. 加法操作符

    • 如果有任一操作数是 NaN,则返回 NaN;
    • 如果是 Infinity 加 Infinity,则返回 Infinity;
    • 如果是-Infinity 加-Infinity,则返回-Infinity;
    • 如果是 Infinity 加-Infinity,则返回 NaN;
    • 如果是+0 加+0,则返回+0;
    • 如果是-0 加+0,则返回+0;
    • 如果是-0 加-0,则返回-0。

    不过,如果有一个操作数是字符串,则要应用如下规则:

    • 如果两个操作数都是字符串,则将第二个字符串拼接到第一个字符串后面;
    • 如果只有一个操作数是字符串,则将另一个操作数转换为字符串,再将两个字符串拼接在一起。

    如果有任一操作数是对象、数值或布尔值,则调用它们的 toString()方法以获取字符串,然后再应用前面的关于字符串的规则。对于 undefined 和 null,则调用 String()函数,分别获取”undefined”和”null”。

  2. 减法操作符

    • 如果两个操作数都是数值,则执行数学减法运算并返回结果。
    • 如果有任一操作数是 NaN,则返回 NaN。
    • 如果是 Infinity 减 Infinity,则返回 NaN。
    • 如果是-Infinity 减-Infinity,则返回 NaN。
    • 如果是 Infinity 减-Infinity,则返回 Infinity。
    • 如果是-Infinity 减 Infinity,则返回-Infinity。
    • 如果是+0 减+0,则返回+0。
    • 如果是+0 减-0,则返回-0。
    • 如果是-0 减-0,则返回+0。
    • 如果有任一操作数是字符串、布尔值、null 或 undefined,则先在后台使用 Number()将其转换为数值,然后再根据前面的规则执行数学运算。如果转换结果是 NaN,则减法计算的结果是NaN。
    • 如果有任一操作数是对象,则调用其 valueOf()方法取得表示它的数值。如果该值是 NaN,则减法计算的结果是 NaN。如果对象没有 valueOf()方法,则调用其 toString()方法,然后再将得到的字符串转换为数值。

3.5.7 关系操作符

关系操作符执行比较两个值的操作,包括小于(<)、大于(>)、小于等于(<=)和大于等于(>=),用法跟数学课上学的一样。

  • 如果操作数都是数值,则执行数值比较。
  • 如果操作数都是字符串,则逐个比较字符串中对应字符的编码。
  • 如果有任一操作数是数值,则将另一个操作数转换为数值,执行数值比较。
  • 如果有任一操作数是对象,则调用其 valueOf()方法,取得结果后再根据前面的规则执行比较。如果没有 valueOf()操作符,则调用 toString()方法,取得结果后再根据前面的规则执行比较。
  • 如果有任一操作数是布尔值,则将其转换为数值再执行比较。

3.5.8 相等操作符

  1. 等于和不等于

    • 如果任一操作数是布尔值,则将其转换为数值再比较是否相等。false 转换为 0,true 转换为 1。
    • 如果一个操作数是字符串,另一个操作数是数值,则尝试将字符串转换为数值,再比较是否相等。
    • 如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf()方法取得其原始值,再根据前面的规则进行比较。

    在进行比较时,这两个操作符会遵循如下规则。

    • null 和 undefined 相等。
    • null 和 undefined 不能转换为其他类型的值再进行比较。
    • 如果有任一操作数是 NaN,则相等操作符返回 false,不相等操作符返回 true。记住:即使两个操作数都是 NaN,相等操作符也返回 false,因为按照规则,NaN 不等于 NaN。
    • 如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回 true。否则,两者不相等。
    表 达 结 果
    null == undefined true
    “NaN” == NaN false
    5 == NaN false
    NaN == NaN false
    NaN != NaN true
    false == 0 true
    true == 1 true
    true == 2 false
    undefined == 0 false
    null == 0 false
    “5” == 5 true
  2. 全等和不全等

let result1 = ("55" == 55); // true,转换后相等
let result2 = ("55" === 55); // false,不相等,因为数据类型不同

3.5.9 条件操作符

let variable = boolean_expression ? true_value : false_value;

let max = (num1 > num2) ? num1 : num2;

3.5.10 赋值操作符

let num = 10;
  • 乘后赋值(*=)
  • 除后赋值(/=)
  • 取模后赋值(%=)
  • 加后赋值(+=)
  • 减后赋值(-=)
  • 左移后赋值(<<=)
  • 右移后赋值(>>=)
  • 无符号右移后赋值(>>>=)

这些操作符仅仅是简写语法,使用它们不会提升性能

3.5.11 逗号操作符

let num1 = 1, num2 = 2, num3 = 3;
    
let num = (5, 1, 4, 8, 0); // num 的值为 0

3.6 语句

3.6.1 if 语句

if (i > 25)
    console.log("Greater than 25."); // 只有一行代码的语句
else {
    console.log("Less than or equal to 25."); // 一个语句块
}

3.6.2 do-while 语句

let i = 0;
do {
    i += 2;
} while (i < 10);

3.6.3 while 语句

let i = 0;
while (i < 10) {
    i += 2;
}

3.6.4 for 语句

let count = 10;
for (let i = 0; i < count; i++) {
    console.log(i);
}

3.6.5 for-in 语句

for (const propName in window) {
    document.write(propName);
}

3.6.6 for-of 语句

for (const el of [2,4,6,8]) { 
    document.write(el); 
}

3.6.7 标签语句

start: for (let i = 0; i < count; i++) { 
    console.log(i); 
}

3.6.8 break 和 continue 语句

break 语句用于立即退出循环,强制执行循环后的下一条语句。而 continue 语句也用于立即退出循环,但会再次从循环顶部开始执行。

3.6.9 with 语句

let qs = location.search.substring(1);
let hostName = location.hostname;
let url = location.href;

//上面代码中的每一行都用到了 location 对象。如果使用 with 语句,就可以少写一些代码:
with(location) {
    let qs = search.substring(1);
    let hostName = hostname;
    let url = href;
}

注意:由于 with 语句影响性能且难于调试其中的代码,通常不推荐在产品代码中使用 with语句。

3.6.10 switch 语句

switch (expression) {
    case value1:
        statement
        break;
    case value2:
        statement
        break;
    case value3:
        statement
        break;
    case value4:
        statement
        break;
    default:
        statement
}

3.7 函数

函数对任何语言来说都是核心组件,因为它们可以封装语句,然后在任何地方、任何时间执行。ECMAScript 中的函数使用 function 关键字声明,后跟一组参数,然后是函数体。

//是函数的基本语法:
function functionName(arg0, arg1,...,argN) {
    statements
}

//下面是一个例子:
function sayHi(name, message) {
    console.log("Hello " + name + ", " + message);
}

严格模式对函数也有一些限制:

  • 数不能以 eval 或 arguments 作为名称;
  • 数的参数不能叫 eval 或 arguments;
  • 个命名参数不能拥有同一个名称。

如果违反上述规则,则会导致语法错误,代码也不会执行

3.8 小结

  • ECMAScript 中的基本数据类型包括 Undefined、Null、Boolean、Number、String 和 Symbol。
  • 与其他语言不同,ECMAScript 不区分整数和浮点值,只有 Number 一种数值数据类型。
  • Object 是一种复杂数据类型,它是这门语言中所有对象的基类。
  • 严格模式为这门语言中某些容易出错的部分施加了限制。
  • ECMAScript 提供了 C 语言和类 C 语言中常见的很多基本操作符,包括数学操作符、布尔操作符、
    关系操作符、相等操作符和赋值操作符等。
  • 这门语言中的流控制语句大多是从其他语言中借鉴而来的,比如 if 语句、for 语句和 switch
    语句等。
  • 不需要指定函数的返回值,因为任何函数可以在任何时候返回任何值。
  • 不指定返回值的函数实际上会返回特殊值 undefined。

原文链接:https://juejin.cn/post/7265149630925258792 作者:慕拾壹

(0)
上一篇 2023年8月10日 上午10:37
下一篇 2023年8月10日 上午10:47

相关推荐

发表回复

登录后才能评论