# 语言基础

从C语言转JavaScript,大部分都能当成C来写。没什么问题的。这部分主要讲它与C语言不同的重要部分。

# 语法基础

JavaScript解释器会自动在它觉得加分号的地方加上分号。但最好还是手动加上,这样看起来比较舒心。而且加分号也有助于在某些情况下提升性能,因为解析器会尝试在合适的位置补上分号以纠正语法错误。

# 严格模式

ECMAScript 5 增加了严格模式(strict mode)的概念。只需要在脚本开头加上这一行。ECMAScript 3 的一些不规范写法在这种模式下会被处理,对于不安全的活动将抛出错误。

"use strict"; 

// 下面是单独指定一个函数为严格模式
function doSomething() {
    "use strict";
    // 函数体
} 
1
2
3
4
5
6
7

# 变量

# var关键字

这是一个非常大的坑。

它可替代C语言中的变量类型,随便你怎么写。如果声明了一个变量却没有赋值,那么变量会保存一个特殊值 undefined

var meessage = "h1";
message = 100;
1
2
  1. var声明作用域

使用 var 操作符定义的变量会成为包含它的函数的局部变量,意味着该变量将在函数退出时被销毁。

function test() {
    var message = "hi"; // 局部变量
}
test();
console.log(message); // 出错!
1
2
3
4
5

不过在函数内定义变量时省略 var 操作符,可以创建一个全局变量。只要调用一次函数 test(),就会定义这个变量,并且可以在函数外部访问到。

function test() {
    message = "hi"; // 全局变量
}
test();
console.log(message); // "hi"
1
2
3
4
5
  1. var变量提升

这是这个关键字最恶心的特性。使用这个关键字声明的变量会自动提升到当前作用域顶部。

function foo() {
    console.log(age);
    var age = 26;
}
foo(); // undefined
1
2
3
4
5

上面这段代码在 ECMAScript 运行时把它看成了这样。

function foo() {
    var age;
    console.log(age);
    age = 26;
}
foo(); // undefined
1
2
3
4
5
6

# let声明

let就比var正常多了,符合所有各大语言的习惯。

let age = 30;
console.log(age);     // 30

if (true) { 
    let age = 26;
    console.log(age); // 26
}
1
2
3
4
5
6
7
  1. 暂时性死区
// age 不会被提升
console.log(age); // ReferenceError:age 没有定义
let age = 26; 
1
2
3
  1. 全局声明

var 关键字不同,使用 let 在全局作用域中声明的变量不会成为 window 对象的属性(var 声明的变量则会)。不过,let 声明仍然是在全局作用域中发生的,为了避免 SyntaxError,必须确保页面不会重复声明同一个变量。

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

let age = 26;
console.log(window.age); // undefined
1
2
3
4
5
  1. 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
1
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
1
2
3
4
5
6
7
8
9
10
11

# const声明

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

const age = 26;
age = 36; // TypeError: 给常量赋值
1
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。当使用 varlet 声明了变量但没有初始化时,就相当于给变量赋予了 undefined 值。

undefined 是一个假值。因此,如果需要,可以用更简洁的方式检测它。

let message; // 这个变量被声明了,只是值为 undefined
// age 没有声明
if (message) {
    // 这个块不会执行
}
if (age) {
    // 这里会报错
}
1
2
3
4
5
6
7
8

# null 对象

null 值表示一个空对象指针,这也是给 typeof 传一个 null 会返回 "object" 的原因。

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

在定义将来要保存对象值的变量时,建议使用 null 来初始化,不要使用其他值。这样,只要检查这个变量的值是不是 null 就可以知道这个变量是否在后来被重新赋予了一个对象的引用。

undefined 一样 null 是一个假值。

# boolean 类型

虽然布尔值只有两个 truefalse,但所有其他 ECMAScript 类型的值都有相应布尔值的等价形式。要将一个其他类型的值转换为布尔值,可以调用特定的 Boolean() 转型函数。

let message = "Hello world!";
let messageAsBoolean = Boolean(message);
1
2
数据类型 转化为true的值 转化为false的值
boolean true false
string 非空字符串 ""
number 非零数值 0、NaN
object 任意对象 null
undefined undefined

# number 类型

  1. 正常的整数
let intNum = 55; // 整数

let octalNum1 = 070;  // 八进制的 56
let octalNum2 = 079;  // 无效的八进制值,当成 79 处理

let hexNum1 = 0xA;    // 十六进制 10
1
2
3
4
5
6
  1. 浮点值

要定义浮点值,数值中必须包含小数点,而且小数点后面必须至少有一个数字。

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

因为存储浮点值使用的内存空间是存储整数值的两倍,所以 ECMAScript 总是想方设法把值转换为整数。

let floatNum1 = 1.;   // 小数点后面没有数字,当成整数 1 处理
let floatNum2 = 10.0; // 小数点后面是零,当成整数 10 处理
1
2

对于非常大或非常小的数值,浮点值可以用科学记数法来表示。

let floatNum = 3.125e7; // 等于 31250000 
1

坑人的地方

浮点值的精确度最高可达 17 位小数,但在算术计算中远不如整数精确。例如,0.10.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.");
} 
1
2
3
  1. 值的范围

ECMAScript 可以表示的最小数值保存在 Number.MIN_VALUE 中,可以表示的最大数值保存在 Number.MAX_VALUE 中。

任何无法表示的负数以 -Infinity(负无穷大)表示,任何无法表示的正数以 Infinity(正无穷大)表示。

通过 isFinite() 函数可以确定是否溢出。

let result = Number.MAX_VALUE + Number.MAX_VALUE;
console.log(isFinite(result)); // false
1
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
1
2
3
4
5

任何涉及 NaN 的操作始终返回 NaN(如 NaN/10),在连续多步计算时这可能是个问题。而且,NaN 不等于包括 NaN 在内的任何值。

console.log(NaN == NaN); // false 
1

判断函数 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 
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 
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,解释为十六进制整数
1
2
3
4
5
6

parseInt() 猜得到你会骂它,所以它能接受第二个参数,传入的是想要转化成的进制。

let num  = parseInt("0xAF", 16);   // 175
let num1 = parseInt("AF", 16);     // 175
let num2 = parseInt("AF");         // NaN
1
2
3
# parseFloat()

parseFloat() 函数的工作方式跟 parseInt() 函数类似,都是从位置 0 开始检测每个字符。同样,它也是解析到字符串末尾或者解析到一个无效的浮点数值字符为止。

这意味着第一次出现的小数点是有效的,但第二次出现的小数点就无效了,此时字符串的剩余字符都会被忽略。因此,"22.34.5" 将转换成 22.34

# string 类型

只要引号不用错,不会有人来打你。

let firstName = "John";
let lastName  = 'Jacob';
let lastName  = `Jingleheimerschmidt`;
1
2
3

# 不可变的特点

ECMAScript 中的字符串是不可变的(immutable),意思是一旦创建,它们的值就不能变了。

let lang = "Java";
lang = lang + "Script";
1
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"
1
2
3
4

toString() 方法可见于数值、布尔值、对象和字符串值。(字符串值也有 toString() 方法,该方法只是简单地返回自身的一个副本。)nullundefined 值没有 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" 
1
2
3
4
5
6

如果你不确定一个值是不是 nullundefined,可以使用 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" 
1
2
3
4
5
6
7
8

提示

因为 nullundefined 没有 toString() 方法,所以 String() 方法就直接返回了这两个值的字面量文本。

# 模板字符串

模板字面量会保留换行字符,可以跨行定义字符串。由于模板字面量会保持反引号内部的空格,因此在使用时要格外注意。格式正确的模板字符串看起来可能会缩进不当。

// 这个模板字面量在换行符之后有 25 个空格符
let myTemplateLiteral = `first line
                         second line`;
console.log(myTemplateLiteral.length); // 47
1
2
3
4

# 字符串插值

const data = `This is a data:${data}`;
1

# 模板字符串标签函数

诡异

这东西太诡异了,可能是我菜,目前我没怎么用上。

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" 
1
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
1
2
3
4

# symbol 类型

Symbol(符号)是 ECMAScript 6 新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。唯一的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。

# 基本使用

符号需要使用 Symbol() 函数初始化。

let sym = Symbol();
console.log(typeof sym); // symbol
1
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
1
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);
1
2
3
4
5

# 使用全局符号注册表

Symbol.for() 对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。

let A = Symbol.for('foo');  // 创建新符号
let B = Symbol.for('foo');  // 重用已有符号

console.log(A === B);       // true
1
2
3
4

要注意

即使采用相同的符号描述,在全局注册表中定义的符号跟使用 Symbol() 定义的符号也并不等同。

let A = Symbol('foo');
let B = Symbol.for('foo');
console.log(A === B);        // false
1
2
3

全局注册表中的符号必须使用字符串键来创建,因此作为参数传给 Symbol.for() 的任何值都会被转换为字符串。

注册表中使用的键同时也会被用作符号描述。

let A = Symbol.for();
console.log(A); // Symbol(undefined) 
1
2

可以使用 Symbol.keyFor() 来查询全局注册表。

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

// 创建普通符号
let s2 = Symbol('bar');
console.log(Symbol.keyFor(s2)); // undefined
1
2
3
4
5
6
7

......

# object 类型

ECMAScript 中的对象其实就是一组数据和功能的集合。对象通过 new 操作符后跟对象类型的名称来创建。开发者可以通过创建 Object 类型的实例来创建自己的对象,然后再给对象添加属性和方法。

let o = new Object(); 
1

每个 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 
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" 
1
2
3
4

# ! 和 &&

它们都特别好玩!

if (!user) {
    console.log(`user is empty.`);
}
1
2
3
!user && console.log(`user is empty.`);
1

注意啊,它们是等价的。

# **

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
1
2
3
4

# 语句

同样与C语言相同的部分就默认都会了......
1

# for-in

for-in 语句是一种严格的迭代语句,用于枚举对象中的非符号键属性。

for (const propName in window) {
    document.write(propName);
} 
1
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);
}
1
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;
}
1
2
3
4
5
6
7
8
9
10
11

# 函数

后面再和面向对象一起介绍吧。现在只需要记住它能这么用,不用像C语言一样写返回值。

function test(){
    // 要执行的语句
}
1
2
3

# 小结

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