一、JS简介
JS既可以运行在浏览器中,也可以运行在服务器端,甚至可以在任意搭载了Javascript引擎的设备中执行。
现代的 JavaScript 是一种“安全的”编程语言。它不提供对内存或 CPU 的底层访问,因为它最初是为浏览器创建的,不需要这些功能。
1.引擎是如何工作的?
引擎很复杂,但是基本原理很简单。
- 引擎(如果是浏览器,则引擎被嵌入在其中)读取(“解析”)脚本。
- 然后,引擎将脚本转化(“编译”)为机器语言。
- 然后,机器代码快速地执行。
引擎会对流程中的每个阶段都进行优化。它甚至可以在编译的脚本运行时监视它,分析流经该脚本的数据,并根据获得的信息进一步优化机器代码。
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.变量
变量命名
- 变量名称必须仅包含字母,数字,符号
$
和_
。 - 首字符必须非数字。
如果命名包括多个单词,通常采用驼峰式命名法(camelCase)。也就是,单词一个接一个,除了第一个单词,其他的每个单词都以大写字母开头:myVeryLongName
。
有趣的是,美元符号 '$'
和下划线 '_'
也可以用于变量命名。它们是正常的符号,就跟字母一样,没有任何特殊的含义。
3.数据类型
在 JavaScript 中有 8 种基本的数据类型(译注:7 种原始类型和 1 种引用类型)。
数学运算是安全的
在 JavaScript 中做数学运算是安全的。我们可以做任何事:除以 0,将非数字字符串视为数字,等等。
脚本永远不会因为一个致命的错误(“死亡”)而停止。最坏的情况下,我们会得到 NaN
的结果。
在 JavaScript 中,“number” 类型无法表示大于 (253-1)
(即 9007199254740991
),或小于 -(253-1)
的整数。这是其内部表示形式导致的技术限制。
String类型
在 JavaScript 中,有三种包含字符串的方式。
- 双引号:
"Hello"
. - 单引号:
'Hello'
. - 反引号:
Hello
.
null类型
相比较于其他编程语言,JavaScript 中的 null
不是一个“对不存在的 object
的引用”或者 “null 指针”。
JavaScript 中的 null
仅仅是一个代表“无”、“空”或“值未知”的特殊值。
上面的代码表示 age
是未知的。
JavaScript的最初版本是这样区分的:null是一个表示”无”的对象,转为数值时为0;undefined是一个表 示”无”的原始值,转为数值时为NaN。
现在的用法:
null表示”没有对象”,即该处不应该有值。
(1) 作为函数的参数,表示该函数的参数不是对象。
(2) 作为对象原型链的终点
undefined表示”缺少值”,就是此处应该有一个值,但是还没有定义。
typeof运算符
typeof
运算符返回参数的类型。当我们想要分别处理不同类型值的时候,或者想快速进行数据类型检验时,非常有用。
它支持两种语法形式:
- 作为运算符:
typeof x
。 - 函数形式:
typeof(x)
。
1 | typeof undefined // "undefined" |
null
绝对不是一个 object
。null
有自己的类型,它是一个特殊值。
并非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
、空字符串、null
、undefined
和NaN
)将变为false
。 - 其他值变成
true
。
1 | "" + 1 + 0 // '10' |
5.值的比较
普通的相等性检查 ==
存在一个问题,它不能区分出 0
和 false
,也同样无法区分空字符串和 false
严格相等运算符 ===
在进行比较时不会做任何的类型转换。
1 | alert( null > 0 ); // (1) false |
为什么会出现这种反常结果,这是因为相等性检查 ==
和普通比较符 > < >= <=
的代码逻辑是相互独立的。进行值的比较时,null
会被转化为数字,因此它被转化为了 0
。这就是为什么(3)中 null >= 0
返回值是 true,(1)中 null > 0
返回值是 false。
另一方面,undefined
和 null
在相等性检查 ==
中不会进行任何的类型转换,它们有自己独立的比较规则,所以除了它们之间互等外,不会等于任何其他的值。这就解释了为什么(2)中 null == 0
会返回 false。
除非你非常清楚自己在做什么,否则永远不要使用 >= > < <=
去比较一个可能为 null/undefined
的变量。对于取值可能是 null/undefined
的变量,请按需要分别检查它的取值情况。
1 | 5 > 4 // true |
6.空值合并运算符
如果第一个参数不是 null/undefined
,则 ??
返回第一个参数。否则,返回第二个参数。空值合并运算符并不是什么全新的东西。它只是一种获得两者中的第一个“已定义的”值的不错的语法。
与||比较
它们之间重要的区别是:
||
返回第一个 真 值。??
返回第一个 已定义的 值。
换句话说,||
无法区分 false
、0
、空字符串 ""
和 null/undefined
。它们都一样 —— 假值(falsy values)。如果其中任何一个是 ||
的第一个参数,那么我们将得到第二个参数作为结果。
优先级
??
运算符的优先级相当低:在 MDN table 中为 5
。因此,??
在 =
和 ?
之前计算,但在大多数其他运算符(例如,+
和 *
)之后计算。
因此,如果我们需要在还有其他运算符的表达式中使用 ??
进行取值,需要考虑加括号
7.函数
默认值
如果未提供参数,那么其默认值则是 undefined
。
如果我们想在本示例中设定“默认”的 text
,那么我们可以在 =
之后指定它:
1 | function showMessage(from, text = "no text given") { |
现在如果 text
参数未被传递,它将会得到值 "no text given"
。
在 JavaScript 中,函数是一个值,所以我们可以把它当成值对待。上面代码显示了一段字符串值,即函数的源码。
的确,在某种意义上说一个函数是一个特殊值,我们可以像 sayHi()
这样调用它。
但它依然是一个值,所以我们可以像使用其他类型的值一样使用它。
为什么会有分号?
1 | function sayHi() { |
答案很简单:
- 在代码块的结尾不需要加分号
;
,像if { ... }
,for { }
,function f { }
等语法结构后面都不用加。 - 函数表达式是在语句内部的:
let sayHi = ...;
,作为一个值。它不是代码块而是一个赋值语句。不管值是什么,都建议在语句末尾添加分号;
。所以这里的分号与函数表达式本身没有任何关系,它只是用于终止语句。
一个函数是表示一个“行为”的值
字符串或数字等常规值代表 数据。
函数可以被视为一个 行为(action)。
我们可以在变量之间传递它们,并在需要时运行。
在编程语言中,一等公民可以作为函数参数,可以作为函数返回值,也可以赋值给变量。
三、对象
1.计算属性
当创建一个对象时,我们可以在对象字面量中使用方括号。这叫做 计算属性。
1 | let fruit = prompt("Which fruit to buy?", "apple"); |
这里有个小陷阱:一个名为 __proto__
的属性。我们不能将它设置为一个非对象的值:
1 | let obj = {}; |
我们从代码中可以看出来,把它赋值为 5
的操作被忽略了。
2.属性存在性测试,‘in’操作
相对于其他语言,javascript的对象中有一个需要特别注意的特性:能够被访问任何属性。即使属性不存在也不会报错!读取不存在的属性只会得到 undefined
。所以我们可以很容易地判断一个属性是否存在
1 | let user = {}; |
这里还有一个特别的,检查属性是否存在的操作符 "in"
。
1 | "key" in object |
1 | let user = { name: "John", age: 30 }; |
请注意,in
的左边必须是 属性名。通常是一个带引号的字符串。
in 与 undefined比较
1 | let obj = { |
对象属性排序
对象有顺序吗?换句话说,如果我们遍历一个对象,我们获取属性的顺序是和属性添加时的顺序相同吗?这靠谱吗?
简短的回答是:“有特别的顺序”:整数属性会被进行排序,其他属性则按照创建的顺序显示。
我们可以使用非整数属性名来 欺骗 程序。只需要给每个键名加一个加号 "+"
前缀就行了。
3.对象的引用和复制
与原始类型相比,对象的根本区别之一是对象是“通过引用”被存储和复制的,与原始类型值相反:字符串,数字,布尔值等 —— 始终是以“整体值”的形式被复制的。
4.垃圾回收
JavaScript 中主要的内存管理概念是 可达性。
简而言之,“可达”值是那些以某种方式可访问或可用的值。它们一定是存储在内存中的。
这里列出固有的可达值的基本集合,这些值明显不能被释放。
比方说:
- 当前函数的局部变量和参数。
- 嵌套调用时,当前调用链上所有函数的变量与参数。
- 全局变量。
- (还有一些内部的)
这些值被称作 根(roots)。
如果一个值可以通过引用或引用链从根访问任何其他值,则认为该值是可达的。
主要需要掌握的内容:
- 垃圾回收是自动完成的,我们不能强制执行或是阻止执行。
- 当对象是可达状态时,它一定是存在于内存中的。
- 被引用与可访问(从一个根)不同:一组相互连接的对象可能整体都不可达。
5.this
在 JavaScript 中,this
是“自由”的,它的值是在调用时计算出来的,它的值并不取决于方法声明的位置,而是取决于在“点符号前”的是什么对象。
以“方法”的语法调用函数时:object.method()
,调用过程中的 this
值是 object
。
6.构造器和操作符‘new’
构造函数
当一个函数被使用 new
操作符执行时,它按照以下步骤:
- 一个新的空对象被创建并分配给
this
。这个新对象会被执行[[prototype]]连接 - 函数体执行。通常它会修改
this
,为其添加新的属性。 - 返回
this
的值。
1 | function User(name) { |
这是构造器的主要目的 —— 实现可重用的对象创建代码。
构造器的return
通常,构造器没有 return
语句。它们的任务是将所有必要的东西写入 this
,并自动转换为结果。
但是,如果这有一个 return
语句,那么规则就简单了:
- 如果
return
返回的是一个对象,则返回这个对象,而不是this
。 - 如果
return
返回的是一个原始类型,则忽略。
换句话说,带有对象的 return
返回该对象,在所有其他情况下返回 this
。
例如,这里 return
通过返回一个对象覆盖 this
:
1 | function BigUser() { |
7.可选链?.
‘不存在属性’的问题
我们大多数用户的地址都存储在 user.address
中,街道地址存储在 user.address.street
中,但有些用户没有提供这些信息。
在这种情况下,当我们尝试获取 user.address.street
,而该用户恰好没提供地址信息,我们则会收到一个错误:
1 | let user = {}; // 一个没有 "address" 属性的 user 对象 |
可选链
为了简明起见,在本文接下来的内容中,我们会说如果一个属性既不是 null
也不是 undefined
,那么它就“存在”。
换句话说,例如 value?.prop
:
- 如果
value
存在,则结果与value.prop
相同, - 否则(当
value
为undefined/null
时)则返回undefined
。
下面这是一种使用 ?.
安全地访问 user.address.street
的方式:
1 | let user = {}; // user 没有 address 属性 |
我们可以使用 ?.
来安全地读取或删除,但不能写入
8.Sysmbol类型
根据规范,对象的属性键只能是字符串类型或者 Symbol 类型。不是 Number,也不是 Boolean,只有字符串或 Symbol 这两种类型。
Symbol
‘symbol’值表示唯一的标识符
可以使用symbol() 创建这种类型的值
1 | // id 是 symbol 的一个实例化对象 |
创建时,我们可以给 Symbol 一个描述(也称为 Symbol 名),这在代码调试时非常有用:
1 | // id 是描述为 "id" 的 Symbol |
Symbol 保证是唯一的。即使我们创建了许多具有相同描述的 Symbol,它们的值也是不同。描述只是一个标签,不影响任何东西。
例如,这里有两个描述相同的 Symbol —— 它们不相等:
1 | let id1 = Symbol("id"); |
Symbol不会被自动转换为字符串
JavaScript 中的大多数值都支持字符串的隐式转换。例如,我们可以 alert
任何值,都可以生效。Symbol 比较特殊,它不会被自动转换。
例如,这个 alert
将会提示出错:
1 | let id = Symbol("id"); |
如果我们真的想显示一个 Symbol,我们需要在它上面调用 .toString()
,如下所示:
1 | let id = Symbol("id"); |
或者获取 symbol.description
属性,只显示描述(description):
1 | let id = Symbol("id"); |
隐藏属性
Symbol 允许我们创建对象的“隐藏”属性,代码的任何其他部分都不能意外访问或重写这些属性。
例如,如果我们使用的是属于第三方代码的 user
对象,我们想要给它们添加一些标识符。
我们可以给它们使用 Symbol 键:
1 | let user = { // 属于另一个代码 |
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 | // 从全局注册表中读取 |
Symbol.keyFor
对于全局 Symbol,不仅有 Symbol.for(key)
按名字返回一个 Symbol,还有一个反向调用:Symbol.keyFor(sym)
,它的作用完全反过来:通过全局 Symbol 返回一个名字。
1 | // 通过 name 获取 Symbol |
Symbol.keyFor
内部使用全局 Symbol 注册表来查找 Symbol 的键。所以它不适用于非全局 Symbol。如果 Symbol 不是全局的,它将无法找到它并返回 undefined
。
也就是说,任何 Symbol 都具有 description
属性。
四、数据类型
函数 toFixed(n) 将数字舍入到小数点后 n
位,并以字符串形式返回结果。
有一个特殊的内建方法 Object.is
,它类似于 ===
一样对值进行比较,但它对于两种边缘情况更可靠:
- 它适用于
NaN
:Object.is(NaN,NaN)=== true
,这是件好事。 - 值
0
和-0
是不同的:Object.is(0,-0)=== false
,从技术上讲这是对的,因为在内部,数字的符号位可能会不同,即使其他所有位均为零。
在所有其他情况下,Object.is(a,b)
与 a === b
相同。
这种比较方式经常被用在 JavaScript 规范中。当内部算法需要比较两个值是否完全相同时,它使用 Object.is
(内部称为 SameValue)。
结论:在处理小数时避免相等性检查。
在 if
测试中 indexOf
有一点不方便。我们不能像这样把它放在 if
中:
1 | let str = "Widget with id"; |
上述示例中的 alert
不会显示,因为 str.indexOf("Widget")
返回 0
(意思是它在起始位置就查找到了匹配项)。是的,但是 if
认为 0
表示 false
。
因此我们应该检查 -1
,像这样:
1 | let str = "Widget with id"; |
方法 | 选择方式…… | 负值参数 |
---|---|---|
slice(start, end) |
从 start 到 end (不含 end ) |
允许 |
substring(start, end) |
start 与 end 之间(包括 start ,但不包括 end ) |
负值代表 0 |
substr(start, length) |
从 start 开始获取长为 length 的字符串 |
允许 start 为负数 |
写一个函数 ucFirst(str)
,并返回首字母大写的字符串 str
,例如:
这里存在一个小问题。如果 str
是空的,那么 str[0]
就是 undefined
,但由于 undefined
并没有 toUpperCase()
方法,因此我们会得到一个错误。
- 使用
str.charAt(0)
,因为它总是会返回一个字符串(可能为空)。 - 为空字符添加测试。
1.数组
splice
数组是对象,所以我们可以尝试使用 delete
:
1 | let arr = ["I", "go", "home"]; |
元素被删除了,但数组仍然有 3 个元素,我们可以看到 arr.length == 3
。
这很正常,因为 delete obj.key
是通过 key
来移除对应的值。对于对象来说是可以的。但是对于数组来说,我们通常希望剩下的元素能够移动并占据被释放的位置。我们希望得到一个更短的数组。
arr.splice 方法可以说是处理数组的瑞士军刀。它可以做所有事情:添加,删除和插入元素。
1 | arr.splice(start[, deleteCount, elem1, ..., elemN]) |
slice
1 | arr.slice([start], [end]) |
它会返回一个新数组,将所有从索引 start
到 end
(不包括 end
)的数组项复制到一个新的数组。start
和 end
都可以是负数,在这种情况下,从末尾计算索引。
concat
arr.concat 创建一个新数组,其中包含来自于其他数组和其他项的值。
1 | arr.concat(arg1, arg2...) |
arr.forEach
arr.forEach 方法允许为数组的每个元素都运行一个函数。
1 | arr.forEach(function(item, index, array) { |
如果我们想检查是否包含某个元素,并且不想知道确切的索引,那么 arr.includes
是首选。
此外,includes
的一个非常小的差别是它能正确处理NaN
,而不像 indexOf/lastIndexOf
:
1 | const arr = [NaN]; |
我们已经知道,箭头函数没有自身的 this
。现在我们知道了它们也没有特殊的 arguments
对象。
v1.5.1