一、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 对象。