加载中...
现代JS学习笔记 loading

一、JS简介

JS既可以运行在浏览器中,也可以运行在服务器端,甚至可以在任意搭载了Javascript引擎的设备中执行。

现代的 JavaScript 是一种“安全的”编程语言。它不提供对内存或 CPU 的底层访问,因为它最初是为浏览器创建的,不需要这些功能。

1.引擎是如何工作的?

引擎很复杂,但是基本原理很简单。

  1. 引擎(如果是浏览器,则引擎被嵌入在其中)读取(“解析”)脚本。
  2. 然后,引擎将脚本转化(“编译”)为机器语言。
  3. 然后,机器代码快速地执行。

引擎会对流程中的每个阶段都进行优化。它甚至可以在编译的脚本运行时监视它,分析流经该脚本的数据,并根据获得的信息进一步优化机器代码。

2.同源策略

不同的标签页/窗口之间通常互不了解。有时候,也会有一些联系,例如一个标签页通过 JavaScript 打开的另外一个标签页。但即使在这种情况下,如果两个标签页打开的不是同一个网站(域名、协议或者端口任一不相同的网站),它们都不能相互通信。

这就是所谓的“同源策略”。为了解决“同源策略”问题,两个标签页必须 包含一些处理这个问题的特定的 JavaScript 代码,并均允许数据交换。本教程会讲到这部分相关的知识。

这个限制也是为了用户的信息安全。例如,用户打开的 http://anysite.com 网页必须不能访问 http://gmail.com(另外一个标签页打开的网页)也不能从那里窃取信息。

二、JS基础知识

1.现代模式 “use strict”

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

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

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

没有类似于 "no use strict" 这样的指令可以使程序返回默认模式。 没有办法取消 use strict

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

因此,目前我们欢迎将 "use strict"; 写在脚本的顶部。稍后,当你的代码全都写在了 class 和 module 中时,你则可以将 "use strict"; 这行代码省略掉。

2.变量

变量命名

  1. 变量名称必须仅包含字母,数字,符号 $_
  2. 首字符必须非数字。

如果命名包括多个单词,通常采用驼峰式命名法(camelCase)。也就是,单词一个接一个,除了第一个单词,其他的每个单词都以大写字母开头:myVeryLongName

有趣的是,美元符号 '$' 和下划线 '_' 也可以用于变量命名。它们是正常的符号,就跟字母一样,没有任何特殊的含义。

3.数据类型

在 JavaScript 中有 8 种基本的数据类型(译注:7 种原始类型和 1 种引用类型)。

数学运算是安全的

在 JavaScript 中做数学运算是安全的。我们可以做任何事:除以 0,将非数字字符串视为数字,等等。

脚本永远不会因为一个致命的错误(“死亡”)而停止。最坏的情况下,我们会得到 NaN 的结果。

在 JavaScript 中,“number” 类型无法表示大于 (253-1)(即 9007199254740991),或小于 -(253-1) 的整数。这是其内部表示形式导致的技术限制。

String类型

在 JavaScript 中,有三种包含字符串的方式。

  1. 双引号:"Hello".
  2. 单引号:'Hello'.
  3. 反引号:Hello.

null类型

相比较于其他编程语言,JavaScript 中的 null 不是一个“对不存在的 object 的引用”或者 “null 指针”。

JavaScript 中的 null 仅仅是一个代表“无”、“空”或“值未知”的特殊值。

上面的代码表示 age 是未知的。

JavaScript的最初版本是这样区分的:null是一个表示”无”的对象,转为数值时为0;undefined是一个表 示”无”的原始值,转为数值时为NaN。

现在的用法:

null表示”没有对象”,即该处不应该有值。

(1) 作为函数的参数,表示该函数的参数不是对象。

(2) 作为对象原型链的终点

undefined表示”缺少值”,就是此处应该有一个值,但是还没有定义。

typeof运算符

typeof 运算符返回参数的类型。当我们想要分别处理不同类型值的时候,或者想快速进行数据类型检验时,非常有用。

它支持两种语法形式:

  1. 作为运算符:typeof x
  2. 函数形式:typeof(x)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typeof undefined // "undefined"

typeof 0 // "number"

typeof 10n // "bigint"

typeof true // "boolean"

typeof "foo" // "string"

typeof Symbol("id") // "symbol"

typeof Math // "object" (1)

typeof null // "object" (2)

typeof alert // "function" (3)

null 绝对不是一个 objectnull 有自己的类型,它是一个特殊值。

并非JavaScript中的所有内容都是对象,应该说所有内容都可以充当对象。

原始类型不是 但是有包装函数,可以有对象的属性和方法

4.类型转换

字符串转换

当我们需要一个字符串形式的值时,就会进行字符串转换。比如,alert(value)value 转换为字符串类型,然后显示这个值。我们也可以显式地调用 String(value) 来将 value 转换为字符串类型:

数字类型转换

在算术函数和表达式中,会自动进行 number 类型转换。比如,当把除法 / 用于非 number 类型:

我们也可以使用 Number(value) 显式地将这个 value 转换为 number 类型。

当我们从 string 类型源(如文本表单)中读取一个值,但期望输入一个数字时,通常需要进行显式转换。如果该字符串不是一个有效的数字,转换的结果会是 NaN

number 类型转换规则:

变成……
undefined NaN
null 0
true/false 1/0
string 去掉首尾空格后的纯数字字符串中含有的数字。如果剩余字符串为空,则转换结果为 0。否则,将会从剩余字符串中“读取”数字。当类型转换出现 error 时返回 NaN

布尔型转换

转换规则如下:

  • 直观上为“空”的值(如 0、空字符串、nullundefinedNaN)将变为 false
  • 其他值变成 true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"" + 1 + 0 // '10'
"" - 1 + 0 // -1
true + false // 1
6 / "3" // 2
"2" * "3" // 6
4 + 5 + "px" // '9px'
"$" + 4 + 5 // '$45'
"4" - 2 // 2
"4px" - 2 // NaN
7 / 0 // infinity
" -9 " + 5 // '-9 5'
" -9 " - 5 // -14
null + 1 // 1
undefined + 1 // NaN
" \t \n" - 2 // -2

5.值的比较

普通的相等性检查 == 存在一个问题,它不能区分出 0false,也同样无法区分空字符串和 false

严格相等运算符 === 在进行比较时不会做任何的类型转换。

1
2
3
alert( null > 0 );  // (1) false
alert( null == 0 ); // (2) false
alert( null >= 0 ); // (3) true

为什么会出现这种反常结果,这是因为相等性检查 == 和普通比较符 > < >= <= 的代码逻辑是相互独立的。进行值的比较时,null 会被转化为数字,因此它被转化为了 0。这就是为什么(3)中 null >= 0 返回值是 true,(1)中 null > 0 返回值是 false。

另一方面,undefinednull 在相等性检查 == 中不会进行任何的类型转换,它们有自己独立的比较规则,所以除了它们之间互等外,不会等于任何其他的值。这就解释了为什么(2)中 null == 0 会返回 false。

除非你非常清楚自己在做什么,否则永远不要使用 >= > < <= 去比较一个可能为 null/undefined 的变量。对于取值可能是 null/undefined 的变量,请按需要分别检查它的取值情况。

1
2
3
4
5
6
7
5 > 4  // true
"apple" > "pineapple" // false
"2" > "12" // true
undefined == null // true
undefined === null // false
null == "\n0\n" // false
null === +"\n0\n" // false

6.空值合并运算符

如果第一个参数不是 null/undefined,则 ?? 返回第一个参数。否则,返回第二个参数。空值合并运算符并不是什么全新的东西。它只是一种获得两者中的第一个“已定义的”值的不错的语法。

与||比较

它们之间重要的区别是:

  • || 返回第一个 值。
  • ?? 返回第一个 已定义的 值。

换句话说,|| 无法区分 false0、空字符串 ""null/undefined。它们都一样 —— 假值(falsy values)。如果其中任何一个是 || 的第一个参数,那么我们将得到第二个参数作为结果。

优先级

?? 运算符的优先级相当低:在 MDN table 中为 5。因此,??=? 之前计算,但在大多数其他运算符(例如,+*)之后计算。

因此,如果我们需要在还有其他运算符的表达式中使用 ?? 进行取值,需要考虑加括号

7.函数

默认值

如果未提供参数,那么其默认值则是 undefined

如果我们想在本示例中设定“默认”的 text,那么我们可以在 = 之后指定它:

1
2
3
4
5
function showMessage(from, text = "no text given") {
alert( from + ": " + text );
}

showMessage("Ann"); // Ann: no text given

现在如果 text 参数未被传递,它将会得到值 "no text given"

在 JavaScript 中,函数是一个,所以我们可以把它当成值对待。上面代码显示了一段字符串值,即函数的源码。

的确,在某种意义上说一个函数是一个特殊值,我们可以像 sayHi() 这样调用它。

但它依然是一个值,所以我们可以像使用其他类型的值一样使用它。

为什么会有分号?

1
2
3
4
5
6
7
8
function sayHi() {
// ...
}

let sayHi = function() {
// ...
};

答案很简单:

  • 在代码块的结尾不需要加分号 ;,像 if { ... }for { }function f { } 等语法结构后面都不用加。
  • 函数表达式是在语句内部的:let sayHi = ...;,作为一个值。它不是代码块而是一个赋值语句。不管值是什么,都建议在语句末尾添加分号 ;。所以这里的分号与函数表达式本身没有任何关系,它只是用于终止语句。

一个函数是表示一个“行为”的值

字符串或数字等常规值代表 数据

函数可以被视为一个 行为(action)

我们可以在变量之间传递它们,并在需要时运行。

在编程语言中,一等公民可以作为函数参数,可以作为函数返回值,也可以赋值给变量。

三、对象

1.计算属性

当创建一个对象时,我们可以在对象字面量中使用方括号。这叫做 计算属性

1
2
3
4
5
6
7
let fruit = prompt("Which fruit to buy?", "apple");

let bag = {
[fruit]: 5, // 属性名是从 fruit 变量中得到的
};

alert( bag.apple ); // 5 如果 fruit="apple"

这里有个小陷阱:一个名为 __proto__ 的属性。我们不能将它设置为一个非对象的值:

1
2
3
let obj = {};
obj.__proto__ = 5; // 分配一个数字
alert(obj.__proto__); // [object Object] — 值为对象,与预期结果不同

我们从代码中可以看出来,把它赋值为 5 的操作被忽略了。

2.属性存在性测试,‘in’操作

相对于其他语言,javascript的对象中有一个需要特别注意的特性:能够被访问任何属性。即使属性不存在也不会报错!读取不存在的属性只会得到 undefined。所以我们可以很容易地判断一个属性是否存在

1
2
3
let user = {};

alert( user.noSuchProperty === undefined ); // true 意思是没有这个属性

这里还有一个特别的,检查属性是否存在的操作符 "in"

1
"key" in object
1
2
3
4
let user = { name: "John", age: 30 };

alert( "age" in user ); // true,user.age 存在
alert( "blabla" in user ); // false,user.blabla 不存在。

请注意,in 的左边必须是 属性名。通常是一个带引号的字符串。

in 与 undefined比较

1
2
3
4
5
6
7
let obj = {
test: undefined
};

alert( obj.test ); // 显示 undefined,所以属性不存在?

alert( "test" in obj ); // true,属性存在!

对象属性排序

对象有顺序吗?换句话说,如果我们遍历一个对象,我们获取属性的顺序是和属性添加时的顺序相同吗?这靠谱吗?

简短的回答是:“有特别的顺序”:整数属性会被进行排序,其他属性则按照创建的顺序显示。

我们可以使用非整数属性名来 欺骗 程序。只需要给每个键名加一个加号 "+" 前缀就行了。

3.对象的引用和复制

与原始类型相比,对象的根本区别之一是对象是“通过引用”被存储和复制的,与原始类型值相反:字符串,数字,布尔值等 —— 始终是以“整体值”的形式被复制的。

4.垃圾回收

JavaScript 中主要的内存管理概念是 可达性

简而言之,“可达”值是那些以某种方式可访问或可用的值。它们一定是存储在内存中的。

  1. 这里列出固有的可达值的基本集合,这些值明显不能被释放。

    比方说:

    • 当前函数的局部变量和参数。
    • 嵌套调用时,当前调用链上所有函数的变量与参数。
    • 全局变量。
    • (还有一些内部的)

    这些值被称作 根(roots)

  2. 如果一个值可以通过引用或引用链从根访问任何其他值,则认为该值是可达的。

主要需要掌握的内容:

  • 垃圾回收是自动完成的,我们不能强制执行或是阻止执行。
  • 当对象是可达状态时,它一定是存在于内存中的。
  • 被引用与可访问(从一个根)不同:一组相互连接的对象可能整体都不可达。

5.this

在 JavaScript 中,this 是“自由”的,它的值是在调用时计算出来的,它的值并不取决于方法声明的位置,而是取决于在“点符号前”的是什么对象。

以“方法”的语法调用函数时:object.method(),调用过程中的 this 值是 object

6.构造器和操作符‘new’

构造函数

当一个函数被使用 new 操作符执行时,它按照以下步骤:

  1. 一个新的空对象被创建并分配给 this。这个新对象会被执行[[prototype]]连接
  2. 函数体执行。通常它会修改 this,为其添加新的属性。
  3. 返回 this 的值。
1
2
3
4
5
6
7
8
9
function User(name) {
// this = {};(隐式创建)

// 添加属性到 this
this.name = name;
this.isAdmin = false;

// return this;(隐式返回)
}

这是构造器的主要目的 —— 实现可重用的对象创建代码。

构造器的return

通常,构造器没有 return 语句。它们的任务是将所有必要的东西写入 this,并自动转换为结果。

但是,如果这有一个 return 语句,那么规则就简单了:

  • 如果 return 返回的是一个对象,则返回这个对象,而不是 this
  • 如果 return 返回的是一个原始类型,则忽略。

换句话说,带有对象的 return 返回该对象,在所有其他情况下返回 this

例如,这里 return 通过返回一个对象覆盖 this

1
2
3
4
5
6
7
8
function BigUser() {

this.name = "John";

return { name: "Godzilla" }; // <-- 返回这个对象
}

alert( new BigUser().name ); // Godzilla,得到了那个对象

7.可选链?.

‘不存在属性’的问题

我们大多数用户的地址都存储在 user.address 中,街道地址存储在 user.address.street 中,但有些用户没有提供这些信息。

在这种情况下,当我们尝试获取 user.address.street,而该用户恰好没提供地址信息,我们则会收到一个错误:

1
2
3
let user = {}; // 一个没有 "address" 属性的 user 对象

alert(user.address.street); // Error!

可选链

为了简明起见,在本文接下来的内容中,我们会说如果一个属性既不是 null 也不是 undefined,那么它就“存在”。

换句话说,例如 value?.prop

  • 如果 value 存在,则结果与 value.prop 相同,
  • 否则(当 valueundefined/null 时)则返回 undefined

下面这是一种使用 ?. 安全地访问 user.address.street 的方式:

1
2
3
let user = {}; // user 没有 address 属性

alert( user?.address?.street ); // undefined(不报错)

我们可以使用 ?. 来安全地读取或删除,但不能写入

8.Sysmbol类型

根据规范,对象的属性键只能是字符串类型或者 Symbol 类型。不是 Number,也不是 Boolean,只有字符串或 Symbol 这两种类型。

Symbol

‘symbol’值表示唯一的标识符

可以使用symbol() 创建这种类型的值

1
2
// id 是 symbol 的一个实例化对象
let id = Symbol();

创建时,我们可以给 Symbol 一个描述(也称为 Symbol 名),这在代码调试时非常有用:

1
2
// id 是描述为 "id" 的 Symbol
let id = Symbol("id");

Symbol 保证是唯一的。即使我们创建了许多具有相同描述的 Symbol,它们的值也是不同。描述只是一个标签,不影响任何东西。

例如,这里有两个描述相同的 Symbol —— 它们不相等:

1
2
3
4
let id1 = Symbol("id");
let id2 = Symbol("id");

alert(id1 == id2); // false

Symbol不会被自动转换为字符串

JavaScript 中的大多数值都支持字符串的隐式转换。例如,我们可以 alert 任何值,都可以生效。Symbol 比较特殊,它不会被自动转换。

例如,这个 alert 将会提示出错:

1
2
let id = Symbol("id");
alert(id); // 类型错误:无法将 Symbol 值转换为字符串。

如果我们真的想显示一个 Symbol,我们需要在它上面调用 .toString(),如下所示:

1
2
let id = Symbol("id");
alert(id.toString()); // Symbol(id),现在它有效了

或者获取 symbol.description 属性,只显示描述(description):

1
2
let id = Symbol("id");
alert(id.description); // id

隐藏属性

Symbol 允许我们创建对象的“隐藏”属性,代码的任何其他部分都不能意外访问或重写这些属性。

例如,如果我们使用的是属于第三方代码的 user 对象,我们想要给它们添加一些标识符。

我们可以给它们使用 Symbol 键:

1
2
3
4
5
6
7
8
9
let user = { // 属于另一个代码
name: "John"
};

let id = Symbol("id");

user[id] = 1;

alert( user[id] ); // 我们可以使用 Symbol 作为键来访问数据

Object.assign 会同时复制字符串和 symbol 属性

Object.keys(user) 也会忽略它们。这是一般“隐藏符号属性”原则的一部分。如果另一个脚本或库遍历我们的对象,它不会意外地访问到符号属性。

全局Symbol

正如我们所看到的,通常所有的 Symbol 都是不同的,即使它们有相同的名字。但有时我们想要名字相同的 Symbol 具有相同的实体。例如,应用程序的不同部分想要访问的 Symbol "id" 指的是完全相同的属性。

为了实现这一点,这里有一个 全局 Symbol 注册表。我们可以在其中创建 Symbol 并在稍后访问它们,它可以确保每次访问相同名字的 Symbol 时,返回的都是相同的 Symbol。

要从注册表中读取(不存在则创建)Symbol,请使用 Symbol.for(key)

该调用会检查全局注册表,如果有一个描述为 key 的 Symbol,则返回该 Symbol,否则将创建一个新 Symbol(Symbol(key)),并通过给定的 key 将其存储在注册表中。

1
2
3
4
5
6
7
8
// 从全局注册表中读取
let id = Symbol.for("id"); // 如果该 Symbol 不存在,则创建它

// 再次读取(可能是在代码中的另一个位置)
let idAgain = Symbol.for("id");

// 相同的 Symbol
alert( id === idAgain ); // true

Symbol.keyFor

对于全局 Symbol,不仅有 Symbol.for(key) 按名字返回一个 Symbol,还有一个反向调用:Symbol.keyFor(sym),它的作用完全反过来:通过全局 Symbol 返回一个名字。

1
2
3
4
5
6
7
// 通过 name 获取 Symbol
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");

// 通过 Symbol 获取 name
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id

Symbol.keyFor 内部使用全局 Symbol 注册表来查找 Symbol 的键。所以它不适用于非全局 Symbol。如果 Symbol 不是全局的,它将无法找到它并返回 undefined

也就是说,任何 Symbol 都具有 description 属性。

四、数据类型

函数 toFixed(n) 将数字舍入到小数点后 n 位,并以字符串形式返回结果。

有一个特殊的内建方法 Object.is,它类似于 === 一样对值进行比较,但它对于两种边缘情况更可靠:

  1. 它适用于 NaNObject.is(NaN,NaN)=== true,这是件好事。
  2. 0-0 是不同的:Object.is(0,-0)=== false,从技术上讲这是对的,因为在内部,数字的符号位可能会不同,即使其他所有位均为零。

在所有其他情况下,Object.is(a,b)a === b 相同。

这种比较方式经常被用在 JavaScript 规范中。当内部算法需要比较两个值是否完全相同时,它使用 Object.is(内部称为 SameValue)。

结论:在处理小数时避免相等性检查。

if 测试中 indexOf 有一点不方便。我们不能像这样把它放在 if 中:

1
2
3
4
5
let str = "Widget with id";

if (str.indexOf("Widget")) {
alert("We found it"); // 不工作!
}

上述示例中的 alert 不会显示,因为 str.indexOf("Widget") 返回 0(意思是它在起始位置就查找到了匹配项)。是的,但是 if 认为 0 表示 false

因此我们应该检查 -1,像这样:

1
2
3
4
5
let str = "Widget with id";

if (str.indexOf("Widget") != -1) {
alert("We found it"); // 现在工作了!
}
方法 选择方式…… 负值参数
slice(start, end) startend(不含 end 允许
substring(start, end) startend 之间(包括 start,但不包括 end 负值代表 0
substr(start, length) start 开始获取长为 length 的字符串 允许 start 为负数

写一个函数 ucFirst(str),并返回首字母大写的字符串 str,例如:

这里存在一个小问题。如果 str 是空的,那么 str[0] 就是 undefined,但由于 undefined 并没有 toUpperCase() 方法,因此我们会得到一个错误。

  1. 使用 str.charAt(0),因为它总是会返回一个字符串(可能为空)。
  2. 为空字符添加测试。

1.数组

splice

数组是对象,所以我们可以尝试使用 delete

1
2
3
4
5
6
7
8
let arr = ["I", "go", "home"];

delete arr[1]; // remove "go"

alert( arr[1] ); // undefined

// now arr = ["I", , "home"];
alert( arr.length ); // 3

元素被删除了,但数组仍然有 3 个元素,我们可以看到 arr.length == 3

这很正常,因为 delete obj.key 是通过 key 来移除对应的值。对于对象来说是可以的。但是对于数组来说,我们通常希望剩下的元素能够移动并占据被释放的位置。我们希望得到一个更短的数组。

arr.splice 方法可以说是处理数组的瑞士军刀。它可以做所有事情:添加,删除和插入元素。

1
arr.splice(start[, deleteCount, elem1, ..., elemN])

slice

1
arr.slice([start], [end])

它会返回一个新数组,将所有从索引 startend(不包括 end)的数组项复制到一个新的数组。startend 都可以是负数,在这种情况下,从末尾计算索引。

concat

arr.concat 创建一个新数组,其中包含来自于其他数组和其他项的值。

1
arr.concat(arg1, arg2...)

arr.forEach

arr.forEach 方法允许为数组的每个元素都运行一个函数。

1
2
3
arr.forEach(function(item, index, array) {
// ... do something with item
});

如果我们想检查是否包含某个元素,并且不想知道确切的索引,那么 arr.includes 是首选。

此外,includes 的一个非常小的差别是它能正确处理NaN,而不像 indexOf/lastIndexOf

1
2
3
const arr = [NaN];
alert( arr.indexOf(NaN) ); // -1(应该为 0,但是严格相等 === equality 对 NaN 无效)
alert( arr.includes(NaN) );// true(这个结果是对的)

我们已经知道,箭头函数没有自身的 this。现在我们知道了它们也没有特殊的 arguments 对象。

上一篇:
v-model 双向数据绑定实现原理
下一篇:
学习 Vue 原理:响应式
Powered By Valine
v1.5.1
本文目录
本文目录