# 语言基础
从C语言转JavaScript,大部分都能当成C来写。没什么问题的。这部分主要讲它与C语言不同的重要部分。
# 语法基础
JavaScript解释器会自动在它觉得加分号的地方加上分号。但最好还是手动加上,这样看起来比较舒心。而且加分号也有助于在某些情况下提升性能,因为解析器会尝试在合适的位置补上分号以纠正语法错误。
# 严格模式
ECMAScript 5 增加了严格模式(strict mode)的概念。只需要在脚本开头加上这一行。ECMAScript 3 的一些不规范写法在这种模式下会被处理,对于不安全的活动将抛出错误。
"use strict";
// 下面是单独指定一个函数为严格模式
function doSomething() {
"use strict";
// 函数体
}
2
3
4
5
6
7
# 变量
# var关键字
这是一个非常大的坑。
它可替代C语言中的变量类型,随便你怎么写。如果声明了一个变量却没有赋值,那么变量会保存一个特殊值 undefined。
var meessage = "h1";
message = 100;
2
var声明作用域
使用 var 操作符定义的变量会成为包含它的函数的局部变量,意味着该变量将在函数退出时被销毁。
function test() {
var message = "hi"; // 局部变量
}
test();
console.log(message); // 出错!
2
3
4
5
不过在函数内定义变量时省略 var 操作符,可以创建一个全局变量。只要调用一次函数 test(),就会定义这个变量,并且可以在函数外部访问到。
function test() {
message = "hi"; // 全局变量
}
test();
console.log(message); // "hi"
2
3
4
5
var变量提升
这是这个关键字最恶心的特性。使用这个关键字声明的变量会自动提升到当前作用域顶部。
function foo() {
console.log(age);
var age = 26;
}
foo(); // undefined
2
3
4
5
上面这段代码在 ECMAScript 运行时把它看成了这样。
function foo() {
var age;
console.log(age);
age = 26;
}
foo(); // undefined
2
3
4
5
6
# let声明
let就比var正常多了,符合所有各大语言的习惯。
let age = 30;
console.log(age); // 30
if (true) {
let age = 26;
console.log(age); // 26
}
2
3
4
5
6
7
- 暂时性死区
// age 不会被提升
console.log(age); // ReferenceError:age 没有定义
let age = 26;
2
3
- 全局声明
与 var 关键字不同,使用 let 在全局作用域中声明的变量不会成为 window 对象的属性(var 声明的变量则会)。不过,let 声明仍然是在全局作用域中发生的,为了避免 SyntaxError,必须确保页面不会重复声明同一个变量。
var name = 'Matt';
console.log(window.name); // 'Matt'
let age = 26;
console.log(window.age); // undefined
2
3
4
5
for循环中的let声明
在 let 出现之前,for 循环定义的迭代变量会渗透到循环体外部。
for (var i = 0; i < 5; ++i) {
// 循环逻辑
}
console.log(i); // 5
// -----
for (var i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0)
}
// 你可能以为会输出 0、1、2、3、4
// 实际上会输出 5、5、5、5、5
2
3
4
5
6
7
8
9
10
11
12
改成使用 let 之后,JavaScript就显得正常多了。
for (let i = 0; i < 5; ++i) {
// 循环逻辑
}
console.log(i); // ReferenceError: i 没有定义
// -----
for (let i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0)
}
// 会输出 0、1、2、3、4
2
3
4
5
6
7
8
9
10
11
# const声明
const 的行为与 let 基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改 const 声明的变量会导致运行时错误。
const age = 26;
age = 36; // TypeError: 给常量赋值
2
所以把它当成不可变的常量就好。
# 数据类型
ECMAScript 有 6 种简单数据类型(也称为原始类型):Undefined、Null、Boolean、Number、String 和 Symbol
Symbol(符号)是 ECMAScript 6 新增的。还有一种复杂数据类型叫 Object(对象)。Object 是一种无序名值对的集合。
# typeof 操作符
对一个值使用 typeof 操作符会返回下列字符串之一。
"undefined"表示值未定义"boolean"表示值为布尔值"string"表示值为字符串"number"表示值为数值"object"表示值为对象(而不是函数)或null"function"表示值为函数"symbol"表示值为符号
比较诡异的地方
调用 typeof null 返回的是 "object"。这是因为特殊值 null 被认为是一个对空对象的引用。
严格来讲,函数在 ECMAScript 中被认为是对象,并不代表一种数据类型。有 "function" 和 "object" 是为 typeof 操作符能区分他俩。
# undefined 类型
Undefined 类型只有一个值,就是特殊值 undefined。当使用 var 或 let 声明了变量但没有初始化时,就相当于给变量赋予了 undefined 值。
undefined 是一个假值。因此,如果需要,可以用更简洁的方式检测它。
let message; // 这个变量被声明了,只是值为 undefined
// age 没有声明
if (message) {
// 这个块不会执行
}
if (age) {
// 这里会报错
}
2
3
4
5
6
7
8
# null 对象
null 值表示一个空对象指针,这也是给 typeof 传一个 null 会返回 "object" 的原因。
let car = null;
console.log(typeof car); // "object"
2
在定义将来要保存对象值的变量时,建议使用 null 来初始化,不要使用其他值。这样,只要检查这个变量的值是不是 null 就可以知道这个变量是否在后来被重新赋予了一个对象的引用。
与 undefined 一样 null 是一个假值。
# boolean 类型
虽然布尔值只有两个 true 和 false,但所有其他 ECMAScript 类型的值都有相应布尔值的等价形式。要将一个其他类型的值转换为布尔值,可以调用特定的 Boolean() 转型函数。
let message = "Hello world!";
let messageAsBoolean = Boolean(message);
2
| 数据类型 | 转化为true的值 | 转化为false的值 |
|---|---|---|
| boolean | true | false |
| string | 非空字符串 | "" |
| number | 非零数值 | 0、NaN |
| object | 任意对象 | null |
| undefined | 无 | undefined |
# number 类型
- 正常的整数
let intNum = 55; // 整数
let octalNum1 = 070; // 八进制的 56
let octalNum2 = 079; // 无效的八进制值,当成 79 处理
let hexNum1 = 0xA; // 十六进制 10
2
3
4
5
6
- 浮点值
要定义浮点值,数值中必须包含小数点,而且小数点后面必须至少有一个数字。
let floatNum1 = 1.1;
let floatNum2 = 0.1;
let floatNum3 = .1; // 有效,但不推荐
2
3
因为存储浮点值使用的内存空间是存储整数值的两倍,所以 ECMAScript 总是想方设法把值转换为整数。
let floatNum1 = 1.; // 小数点后面没有数字,当成整数 1 处理
let floatNum2 = 10.0; // 小数点后面是零,当成整数 10 处理
2
对于非常大或非常小的数值,浮点值可以用科学记数法来表示。
let floatNum = 3.125e7; // 等于 31250000
坑人的地方
浮点值的精确度最高可达 17 位小数,但在算术计算中远不如整数精确。例如,0.1 加 0.2 得到的不是 0.3,而是 0.300 000 000 000 000 04。
检测两个数值之和是否等于 0.3。如果两个数值分别是 0.05 和 0.25,或者 0.15 和 0.15,那没问题。但如果是 0.1 和 0.2,如前所述,测试将失败。
if (a + b == 0.3) { // 别这么干!
console.log("You got 0.3.");
}
2
3
- 值的范围
ECMAScript 可以表示的最小数值保存在 Number.MIN_VALUE 中,可以表示的最大数值保存在 Number.MAX_VALUE 中。
任何无法表示的负数以 -Infinity(负无穷大)表示,任何无法表示的正数以 Infinity(正无穷大)表示。
通过 isFinite() 函数可以确定是否溢出。
let result = Number.MAX_VALUE + Number.MAX_VALUE;
console.log(isFinite(result)); // false
2
# NaN
有一个特殊的数值叫 NaN,意思是“不是数值”(Not a Number),用于表示本来要返回数值的操作失败了 (而不是抛出错误)。
console.log(0 / 0); // NaN
console.log(-0 / +0); // NaN
console.log(5 / 0); // Infinity
console.log(5 / -0); // -Infinity
2
3
4
5
任何涉及 NaN 的操作始终返回 NaN(如 NaN/10),在连续多步计算时这可能是个问题。而且,NaN 不等于包括 NaN 在内的任何值。
console.log(NaN == NaN); // false
判断函数 isNaN()。
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
2
3
4
5
# 数值转化
有 3 个函数可以将非数值转换为数值:Number()、parseInt()和 parseFloat()。
# Number()
let num1 = Number("Hello world!"); // NaN
let num2 = Number(""); // 0
let num3 = Number("000011"); // 11
let num4 = Number(true); // 1
2
3
4
提示
考虑到用 Number() 函数转换字符串时相对复杂且有点反常规,通常在需要得到整数时可以优先使用 parseInt() 函数。
不过 parseInt() 函数同样诡异。
# parseInt()
"1234blue"会被转换为1234,因为"blue"会被完全忽略。"22.5"会被转换为22,因为小数点不是有效的整数字符。
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,解释为十六进制整数
2
3
4
5
6
parseInt() 猜得到你会骂它,所以它能接受第二个参数,传入的是想要转化成的进制。
let num = parseInt("0xAF", 16); // 175
let num1 = parseInt("AF", 16); // 175
let num2 = parseInt("AF"); // NaN
2
3
# parseFloat()
parseFloat() 函数的工作方式跟 parseInt() 函数类似,都是从位置 0 开始检测每个字符。同样,它也是解析到字符串末尾或者解析到一个无效的浮点数值字符为止。
这意味着第一次出现的小数点是有效的,但第二次出现的小数点就无效了,此时字符串的剩余字符都会被忽略。因此,"22.34.5" 将转换成 22.34。
# string 类型
只要引号不用错,不会有人来打你。
let firstName = "John";
let lastName = 'Jacob';
let lastName = `Jingleheimerschmidt`;
2
3
# 不可变的特点
ECMAScript 中的字符串是不可变的(immutable),意思是一旦创建,它们的值就不能变了。
let lang = "Java";
lang = lang + "Script";
2
变量 lang 一开始包含字符串 "Java"。紧接着,lang 被重新定义为包含 "Java" 和 "Script" 的组合,也就是"JavaScript"。整个过程首先会分配一个足够容纳 10 个字符的空间,然后填充上 "Java" 和 "Script"。最后销毁原始的字符串 "Java" 和字符串 "Script"。
# 某个变量转换为字符串
let age = 11;
let ageAsString = age.toString(); // 字符串"11"
let found = true;
let foundAsString = found.toString(); // 字符串"true"
2
3
4
toString() 方法可见于数值、布尔值、对象和字符串值。(字符串值也有 toString() 方法,该方法只是简单地返回自身的一个副本。)null 和 undefined 值没有 toString() 方法。
特殊的地方
数值调用 toString() 返回数值的十进制字符串表示。而通过传入参数,可以得到数值的二进制、八进制、十六进制,或者其他任何有效基数的字符串表示。
let num = 10;
console.log(num.toString()); // "10"
console.log(num.toString(2)); // "1010"
console.log(num.toString(8)); // "12"
console.log(num.toString(10)); // "10"
console.log(num.toString(16)); // "a"
2
3
4
5
6
如果你不确定一个值是不是 null 或 undefined,可以使用 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"
2
3
4
5
6
7
8
提示
因为 null 和 undefined 没有 toString() 方法,所以 String() 方法就直接返回了这两个值的字面量文本。
# 模板字符串
模板字面量会保留换行字符,可以跨行定义字符串。由于模板字面量会保持反引号内部的空格,因此在使用时要格外注意。格式正确的模板字符串看起来可能会缩进不当。
// 这个模板字面量在换行符之后有 25 个空格符
let myTemplateLiteral = `first line
second line`;
console.log(myTemplateLiteral.length); // 47
2
3
4
# 字符串插值
const data = `This is a data:${data}`;
# 模板字符串标签函数
诡异
这东西太诡异了,可能是我菜,目前我没怎么用上。
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';
}
// 另一个等价函数
function VTSimpleTag(strings, ...expressions) {
console.log(strings);
for (const expression of expressions) {
console.log(expression);
}
return 'foobar';
}
let taggedResult = simpleTag`${a} + ${b} = ${a + b}`;
// 此时会输出
// ["", " + ", " = ", ""]
// 6
// 9
// 15
// 输出刚刚函数的返回值
console.log(taggedResult); // "foobar"
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
27
28
29
# 原始字符串
String.raw 标签函数可以直接获取原始的模板字面量内容(如换行符或 Unicode 字符),而不是被转换后的字符表示。
// Unicode 示例
// \u00A9 是版权符号
console.log(`\u00A9`); // ©
console.log(String.raw`\u00A9`); // \u00A9
2
3
4
# symbol 类型
Symbol(符号)是 ECMAScript 6 新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。唯一的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
# 基本使用
符号需要使用 Symbol() 函数初始化。
let sym = Symbol();
console.log(typeof sym); // symbol
2
调用 Symbol() 函数时,也可以传入一个字符串参数作为对符号的描述(description),将来可以通过这个字符串来调试代码。但是,这个字符串参数与符号定义或标识完全无关。
let A = Symbol();
let B = Symbol();
let C = Symbol('foo');
let D = Symbol('foo');
console.log(A == B); // false
console.log(C == D); // false
2
3
4
5
6
7
8
按照规范,你只要创建 Symbol() 实例并将其用作对象的新属性,就可以保证它不会覆盖已有的对象属性,无论是符号属性还是字符串属性。注意,这里不能使用 new 关键字区包装对象。
let genericSymbol = Symbol();
console.log(genericSymbol); // Symbol()
let fooSymbol = Symbol('foo');
console.log(fooSymbol); // Symbol(foo);
2
3
4
5
# 使用全局符号注册表
Symbol.for() 对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。
let A = Symbol.for('foo'); // 创建新符号
let B = Symbol.for('foo'); // 重用已有符号
console.log(A === B); // true
2
3
4
要注意
即使采用相同的符号描述,在全局注册表中定义的符号跟使用 Symbol() 定义的符号也并不等同。
let A = Symbol('foo');
let B = Symbol.for('foo');
console.log(A === B); // false
2
3
全局注册表中的符号必须使用字符串键来创建,因此作为参数传给 Symbol.for() 的任何值都会被转换为字符串。
注册表中使用的键同时也会被用作符号描述。
let A = Symbol.for();
console.log(A); // Symbol(undefined)
2
可以使用 Symbol.keyFor() 来查询全局注册表。
// 创建全局符号
let s = Symbol.for('foo');
console.log(Symbol.keyFor(s)); // foo
// 创建普通符号
let s2 = Symbol('bar');
console.log(Symbol.keyFor(s2)); // undefined
2
3
4
5
6
7
......
# object 类型
ECMAScript 中的对象其实就是一组数据和功能的集合。对象通过 new 操作符后跟对象类型的名称来创建。开发者可以通过创建 Object 类型的实例来创建自己的对象,然后再给对象添加属性和方法。
let o = new Object();
每个 Object 实例都有如下属性和方法。因为在 ECMAScript 中 Object 是所有对象的基类,后面会介绍对象间的继承机制。
constructor:用于创建当前对象的函数。在前面的例子中,这个属性的值就是Object()函数。hasOwnProperty(propertyName):用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查的属性名必须是字符串(如o.hasOwnProperty("name"))或符号。isPrototypeOf(object):用于判断当前对象是否为另一个对象的原型。(后面也会讲到原型)propertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用for-in语句枚举toLocaleString():返回对象的字符串表示,该字符串反映对象所在的本地化执行环境。toString():返回对象的字符串表示。valueOf():返回对象对应的字符串、数值或布尔值表示。通常与toString()的返回值相同。
# 操作符
写在前面
和C语言差不多吧,前自增、后自增......这里也只会提与C语言不太一样的地方
# + 和 -
在一个字符串之间使用 + 或 -,会先尝试将字符串转化成对应的数字。
let s1 = "01";
let s2 = "1.1";
let s3 = "z";
let b = false;
let f = 1.1;
let o = {
valueOf() {
return -1;
}
};
s1 = -s1; // 值变成数值-1
s2 = -s2; // 值变成数值-1.1
s3 = -s3; // 值变成 NaN
b = -b; // 值变成数值 0
f = -f; // 变成-1.1
o = -o; // 值变成数值 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
注意
ECMAScript 中最常犯的一个错误,就是忽略加法操作中涉及的数据类型。
let num1 = 5;
let num2 = 10;
let message = "The sum of 5 and 10 is " + num1 + num2;
console.log(message); // "The sum of 5 and 10 is 510"
2
3
4
# ! 和 &&
它们都特别好玩!
if (!user) {
console.log(`user is empty.`);
}
2
3
!user && console.log(`user is empty.`);
注意啊,它们是等价的。
# **
ECMAScript 7 新增了指数操作符,Math.pow() 现在有了自己的操作符 **,用哪个都行,结果是一样的。
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
2
3
4
# 语句
同样与C语言相同的部分就默认都会了......
# for-in
for-in 语句是一种严格的迭代语句,用于枚举对象中的非符号键属性。
for (const propName in window) {
document.write(propName);
}
2
3
这个例子使用 for-in 循环显示了 BOM 对象 window 的所有属性。每次执行循环,都会给变量 propName 赋予一个 window 对象的属性作为值,直到 window 的所有属性都被枚举一遍。与 for 循环一样,这里控制语句中的 const 也不是必需的。但为了确保这个局部变量不被修改,推荐使用 const。
# for-of
for-of 语句是一种严格的迭代语句,用于遍历可迭代对象的元素。
for (const el of [2, 4, 6, 8]) {
document.write(el);
}
2
3
在使用 for-of 语句显示了一个包含 4 个元素的数组中的所有元素。循环会一直持续到将所有元素都迭代完。
# with
注意
一般没人用也不推荐用,看看就好。
with 语句的用途是将代码作用域设置为特定的对象。
let qs = location.search.substring(1);
let hostName = location.hostname;
let url = location.href;
// 等价于
with (location) {
let qs = search.substring(1);
let hostName = hostname;
let url = href;
}
2
3
4
5
6
7
8
9
10
11
# 函数
后面再和面向对象一起介绍吧。现在只需要记住它能这么用,不用像C语言一样写返回值。
function test(){
// 要执行的语句
}
2
3
# 小结
- ECMAScript 中的基本数据类型包括
undefined、null、boolean、number、string和symbol。 - 与其他语言不同,ECMAScript 不区分整数和浮点值,只有
number一种数值数据类型。 object是一种复杂数据类型,它是这门语言中所有对象的基类。- 严格模式为这门语言中某些容易出错的部分施加了限制。
- ECMAScript 提供了 C 语言和类 C 语言中常见的很多基本操作符,包括数学操作符、布尔操作符、关系操作符、相等操作符和赋值操作符等。
- 这门语言中的流控制语句大多是从其他语言中借鉴而来的,比如
if语句、for语句和switch语句等。ECMAScript 中的函数与其他语言中的函数不一样。 - 不需要指定函数的返回值,因为任何函数可以在任何时候返回任何值。
- 函数没返回值实际上会返回特殊值
undefined。