前言
JavaScript 诞生于1995年,之后的几年时间里,浏览器市场发展迅速,浏览器脚本语言也百花齐放,出现了如 JScript 这样的浏览器脚本语言,各自的发展可谓是五花八门。后来,为了规范化发展,网景公司将 JavaScript提交给了ECMA 进行标准化,并将标准名称定为 ECMA-262,执行这个标准的脚本语言统称为 ECMAScript。
从1997年6月 ECMA 采纳的 ECMA-262 第一个版本开始,ECMA-262 前期经历了如下的版本更迭:
- 1998年6月发布 ECMAScript 2.0
- 1999年12月发布 ECMAScript 3.0
- 2007年10月发布 ECMAScript 4.0 草案
- 2009年12月发布 ECMAScript 5.0
- 2011年6月发布 ECMAScript 5.1
- 2015年6月发布 ECMAScript 6
至此 ECMAScript 的递增编号版本结束,从 ECMAScript6 及以后的版本都以年份定版,所以 ECMAScript 6 可以理解为以年份定版的 ECMAScript 标准的统称。
ECMAScript 简称为 ES,原生 JavaScript 本质上是 ECMAScript 标准的一个实现
有的人会习惯以递增版本号来称呼 ECMAScript,所以你会看到有 ES6/ES7/ES8 这样的称呼,但这不是官方的称呼,只是大家的习惯,官方的称呼是 ES2015/ES2016/.../ESNext
1. 变量定义
ECMAScript 是弱类型语言标准,所以 JavaScript 是一门弱类型语言。
弱类型语言:定义变量时,无需指定变量的数据类型,如 JavaScript、Python
强类型语言:定义变量时,必须指定变量的数据类型,如 C/C++、Java
在了解变量定义之前,首先要理解 作用域 的概念。在 ES6 之前,只有 全局作用域 和 函数作用域 两种 (不考虑不常用的语法);ES6 中新增了 模块作用域 、块级作用域。
- 全局作用域:即根作用域,在这里定义的所有变量可以在任何地方被访问
- 函数作用域:即一个函数所限定的作用域范围
- 模块作用域,即一个 ES6 模块所限定的作用域范围
- ES6 模块:以
<script type="module"></script>来标识的 JavaScript 代码块和引入的 JavaScript 代码
- ES6 模块:以
- 块级作用域:即一个
{}代码块所限定的作用域范围- 注:
{}也会被用于对象字面量定义,对象字面量定义时,它不是用来划定作用域的
- 注:
1.1 普通变量
在 ES6 之前,定义普通变量的关键字是 var;ES6 中,提出了 let 和 const 两个新的关键字
var关键字定义的变量特征:- 作用域:全局作用域/函数作用域
- 变量提升:在定义变量之前也可以访问变量,只不过取到的值是
undefined - 定义的变量可被重新赋值
- 同一个作用域内,可重复定义同名变量
let关键字定义的变量特征:- 作用域:块级作用域
- 暂时性死区:在定义变量之前访问变量时,将报
ReferenceError - 定义的变量可被重新赋值
- 同一个作用域内,不允许重复定义同名变量
const关键字定义的变量特征:- 作用域:块级作用域
- 暂时性死区:在定义变量之前访问变量时,将报
ReferenceError - 定义的变量不可被重新赋值,如果变量是一个对象,它的属性可以被重新赋值
- 同一个作用域内,不允许重复定义同名变量
console.log("a=", a); // 这里可以访问到 a 变量,但是值为 undefined
// console.log("b=", b); // 这里访问不到 b 变量, 因为这里是暂时性死区
// console.log("c=", c); // 这里访问不到 c 变量, 因为这里是暂时性死区
var a = 0;
var a = 2; // var 定义的变量,允许重复定义同名变量
const b = { attr: 1 };
// const b = { attr: 2 }; // 同一个作用域内, const 不允许重复定义同名变量
// b = { attr: 3 }; // const 定义的变量不允许被重新赋值
b.attr = 4; // const 定义的变量,为对象时,它的属性值可以被修改
let c = 5;
// let c = 6; // 同一个作用域内, let 不允许重复定义同名变量
c = 7; // let 定义的变量可以被重新赋值
1.2 函数定义
使用关键字 function。
函数定义有两种方式,一种是函数声明,即使用关键字 function 来声明一个函数;一种是函数表达式,即把一个函数声明赋值给一个变量。
-
函数声明:通过
functuon fn_name (p1, p2, ...) {}的声明方式来声明函数- 存在变量提升,在可访问的作用域内,可以在函数声明前调用函数。
- 不可以定义为匿名函数
-
函数表达式:将函数声明赋值给一个变量,此时的函数声明可以不写函数名,即使用匿名函数
- 像普通变量一样被定义,只不过变量的值是一个函数
- 函数可以是匿名函数
- 调用函数表达式时,只能通过变量名来调用,即使定义时使用的是命名函数
ES6 增加了一种新的函数:箭头函数。只能通过函数表达式的方式声明箭头函数。
fn1(1, 2); // 函数声明的函数可以在声明前访问,不会报错
// fn2(3, 4); // 函数表达式声明的函数,不能在声明前访问,这里是暂时性死区,即使用 var 定义,这里也只是 undefined
// 函数声明
function fn1(p1, p2) {
console.log("p1=", p1);
console.log("p2=", p2);
}
// 匿名函数表达式
const fn2 = function(p1, p2) {
console.log("p1=", p1);
console.log("p2=", p2);
}
// 具名函数表达式
const fn3 = function f3(p1, p2) {
console.log("p1=", p1);
console.log("p2=", p2);
}
// 箭头函数
const fn4 = (p1, p2) => {
console.log("p1=", p1);
console.log("p2=", p2);
}
fn3(5, 6); // 调用具名函数表达式时,也应该用变量名,而不是函数名
fn4(7, 8); // 箭头函数表达式和 function 定义的函数表达式使用方式相同
箭头函数和 function 函数的区别:
this指向不同:箭头函数里的this值取决于函数调用所在作用域的this值,function 函数里的this值取决于函数的调用方式- 箭头函数不可被实例化,function (函数声明、函数表达式)函数可以被实例化
function 函数内部的 this 指向规则,将在后面补充
1.3 class 定义
使用关键字:class。
了解 JavaScript 背景我们知道,JavaScript 有借鉴 Java 的一些特性,其中就包括面向对象,所以才会有 this,但 JavaScript 的面向对象是基于原型链的,所以在 ES6 之前,要想实现继承是一件非常繁琐的事情,如果你对这个感兴趣,推荐你参阅文章 JavaScript常用八种继承方案。
ES6 之后,ECMAScript 引入了 class 这个语法糖来封装之前的繁琐实现,这让 JavaScript 的面向对象从编码层面更像 Java、C++ 那样的 OOP 语言。
ECMAScript class 支持 继承、静态属性、静态方法,不支持私有(private)属性、保护(protected)属性、接口。
ECMAScript 不支持的这些 OOP 特性,让遵循 ECMAScript 的编程语言显得没那么真正地 “面向对象”。而 Typescript 支持所有的 OOP 特性,且 Typescript 可以被编译成 JavaScript,所以如果想在前端真正以形似 OOP 的方式去开发,还得用 Typescript。
class Parent {
static VER = "1.0.0";
name = "";
constructor(name) {
this.name = name;
}
}
class Child extends Parent {
age = 0;
constructor(name, age) {
super(name);
this.age = age;
}
static elderThan(c1, c2) {
return c1.age - c2.age > 0;
}
introduction() {
console.log(`大家好,我叫 ${this.name}, 今年 ${this.age} 岁了。`); // 这里使用的是模板字符串
}
}
const p1 = new Parent("p1");
const c1 = new Child("c1", 12);
const c2 = new Child("c2", 10);
console.log("Parent version =", Parent.VER); // 静态属性通过 "类名.属性名" 调用
console.log("c1 name =", c1.name); // 实例属性通过 "实例.属性名" 调用
console.log("c1 比 c2 大吗?", Child.elderThan(c1, c2)); // 静态方法通过 "类名.方法名" 调用
c1.introduction(); // 实例方法通过 "实例.方法名" 调用
c2.introduction(); // 实例方法通过 "实例.方法名" 调用
1.4 模块定义
在 ECMAScript 中,起初没有 模块 这个概念,随着 JavaScript 承载的能力越来越多、JavaScript 应用程序越来越复杂,开发者们感觉到了 JavaScript 的一些非常突出的问题,比如命名冲突。
在 ES6 之前,开发者社区尝试了非常多的模块化规范,最终脱颖而出的是 CommonJS, AMD, UMD。
- CommonJS (.cjs):Node.js 采用,没有被浏览器段采纳与实现。该规范约定,每一个文件就是一个模块,模块可以通过
module.exports这个对象来导出期望导出的内容,模块可以通过require("other_module_name")来导入其他模块导出的内容。 - AMD(Async Module Definition):异步模块定义,浏览器端的一种模块化规范,可通过 RequireJS 来实现。详细的介绍可以参阅文章 Javascript模块化-AMD。
- UMD(Universal Module Definition):通用模块定义,UMD 的目标是让编写的 JavaScript 模块可以在所有的 JavaScript 运行环境中执行。详细介绍和实现可以参阅文章 可能是最详细的UMD模块入门指南。
ECMAScript6 中提出了模块的规范,被称为 ECMAScript module,简称 ESM,通常以 .mjs 作为文件扩展名。
在支持 ECMAScript6 规范的浏览器中,可以直接使用 <script type="module"></script> 来标识一个模块:
<script type="module">
// ES6 的模块代码
</script>
<!-- index.js 中可以使用 ES6 的模块语法 -->
<script type="module" src="./index.js"></script>
使用 ES6 模板的时候,通常一个文件就是一个模块,可以通过 import/export 来导入/导出模块数据。
export
导出分为两种:命名导出、默认导出。
export const a = "a"; // 命名导出
const b = "b";
const c = "c";
export { b, c }; // 命名导出
const d = "d";
export default d; // 默认导出
import
与导出对应,导入也分为两种:命名导入、默认导入。
import { a } from "./a.js"; // 命名导入
import * as aJs from "./a.js"; // 将 a.js 导出的所有内容放到 aJs 这个命名空间下
import d from "./a.js"; 默认导入
console.log("b =", aJs.b); // 通过命名空间使用对应模块的导出属性
console.log("d =", aJs.default); // 通过命名空间使用模块的导出属性时,默认导出只能用 default 这个属性名来获取
ESM 还支持动态导入,格式为
import("./a.js"),动态导入的结果只能通过import返回的 Promise 得到。
2. 数据类型
为什么是值的类型?
ECMAScript 规范是针对动态类型脚本语言的规范,变量的数据类型是不确定的,给变量赋不同的值,变量的数据类型就变成了对应值的类型,因此遵循 ECMAScript 规范的编程语言中,我们通常说的是值的数据类型 (简称 值类型 ),而不是变量的数据类型。
ECMAScript 规范定义了 8 种值类型,分别是:undefined、null、boolean、number、string、bigint、symbol、object。
其中 symbol 是在 ES2015定义的,bigint 是 ES2020 定义的,其他 6 种值类型都是在 ES2015 之前定义的。
这些值类型中,object 为引用值类型,其它 7 种值类型为原始值类型。
原始值类型
原始值类型 可以简单地理解为 “简单的数据”,它们的特性有:
- 复制:按值复制,复制后的变量与原始变量互不影响
- 存储访问:大小固定,保存在栈里,方便快速访问
引用值类型
引用值类型 可以简单地理解为 “复杂的数据”,它们的特性有:
- 复制:按引用复制,复制后的变量与原始变量指向同一个值,对某个变量的属性修改,会反映到另一个变量上
- 存储访问:大小不固定,引用值的本体保存在堆里,引用值保存在栈里
你可以在浏览器的 devtools 里逐行输入如下的代码,初步感受一下 JavaScript 里的原始值类型和引用值类型。
// 原始值类型
let a = 123;
let b = a;
console("a =", a);
console("b =", b);
console("a等于b吗?", a === b); // a等于b吗? true
b = 1234;
console("a =", a);
console("b =", b);
console("a等于b吗?", a === b); // a等于b吗? false
// 引用值类型
const c = { count: 123 };
const d = c;
console.log("c =", c);
console.log("d =", d);
console.log("c等于d吗?", c === d); // c等于d吗? true
d.count = 1234;
console.log("c =", c);
console.log("d =", d);
console.log("c等于d吗?", c === d); // c等于d吗? true
你也可以将这段代码保存到 JavaScript 文件,或者 HTML 文件的 <script> 标签里,然后运行对应的文件,你就能在浏览器的 devtools 看到对应的输出了。
如何运行 JavaScript 代码?参阅文章 运行 JavaScript 代码的三种方式
2.1 undefined 和 null
undefined 和 null 是两个特殊的值类型,这两个类型都只有对应的一个值,分别为 undefined 和 null。
undefined 的含义是 未定义 ,在如下情况下,会获取到 undefined 值:
-
void表达式 -
访问未定义的变量 (不是在 暂时性死区 中的变量访问)
-
访问对象及对象原型链中不存在的属性
-
使用
var和let定义变量后,未给变量赋值就访问这些变量- 也就是说,变量定义了但未赋值时,变量的值会是
undefined
- 也就是说,变量定义了但未赋值时,变量的值会是
由于 undefined 不是 ECMAScript 的保留字,因此在某些情况下它会被全局污染,也就是说,全局可能存在一个变量被命名为了 undefined,此时会导致程序获取到不安全的
undefined。要想在所有情况下都获取到安全的
undefined,可以通过void表达式来获取,因为void任何值得到的都是undefined,常用的一个获取安全undefined的表达式是void 0
null 的含义是 空对象,它通常的使用场景如下:
-
定义一个变量,该变量将在后续被赋值为一个对象,此时建议给这个变量附初始值为
null -
从 JSON 中获取到的某个属性,其值为
null- 通常是后端返回的数据中某些属性值为
null,或者是从 JSON 文件中得到的值为null的属性
- 通常是后端返回的数据中某些属性值为
null 的含义和 undefined 的含义是完全不同的,一定要注意区分。
建议在定义无初值的对象变量时,始终为其赋初始值 null。
console.log("a =", a); // 在定义前就访问,得到的是 undefined
// console.log("b =", b); // 这里是 b 的暂时性死区,访问 b 会报 ReferenceError
var a = 0;
let b;
console.log("b =", b); // 在赋值前就访问,得到的是 undefined
b = 1;
let c = null;
console.log("c =", c); // 如果一个变量后续会被赋值为对象,建议定义时赋初值为 null
c = { count:0 };
console.log("c =", c);
2.2 boolean, number 和 string
上一节介绍的 null 和 undefined 是 ECMAScript 中最简单的两种值类型,这一节介绍的这三个值类型是在日常开发中最常用的。
这三个值类型都是原始值类型,在对这三类原始值执行一些操作时,比如对 number 字面量的变量调用 .toFixed 方法,JavaScript 引擎在底层会先把这个字面量用对应的包装类型进行包装,然后再调用包装类型实例对象的这个方法。
字面量:用形如
const str = "abc";这样直接把值赋值给变量的形式,这个值就被称为字面量。原始值类型的值通常都是用字面量的形式写在表达式和语句里的。
包装类型:原始值类型对应的对象类型。
boolean 值类型对应的包装类型是 Boolean
number 值类型对应的包装类型是 Number
string 值类型对应的包装类型是 String
注意:
undefined和null没有包装类型
const a = 123.45678;
const b = a.toFixed(2);
console.log("a =", a);
console.log("b =", b);
这三类原始值类型的包装类型,除了 boolean 外,都提供了非常多的实例方法,参阅 MDN 的文档:
翻阅这些文档我们可以发现,每一个包装类型都提供了对应的方法、释义、使用示例、兼容性等。但是!等等!这么多属性和方法,怎么可能一下子就记住呢?
是的,死记硬背肯定是无法记住的,我们可以通过属性和方法的含义来记忆。
比如字符串用来表示长度的属性,长度 在英语中是 length,所以字符串用来表示长度的属性就是 length。
比如拆分字符串,拆分 在英语中是 split,所以字符串用来拆分的方法就是 split,拆分字符串的时候肯定要标明根据什么规则来拆分,所以 split 方法的第一个参数就是用来表明拆分规则的,除了这个参数外,设计师还设计了第二个参数,用来限定返回的分割片段数量,这个参数只需要顺带记忆就可以了。
如果用这种联想方法还是记不住怎么办呢?
优质回答:查 MDN 文档。
这是一个非常细致且全面的前端开发文档,里面包含了 HTML 标签、CSS 属性、JavaScript API 等,只要你想查的,几乎都能查到。所以一定要牢记这个网站:MDN。
2.3 symbol 和 bigint
bigint 类型是一个原始值类型,于 ES2020 中引入,目前它所提供的能力仅用于超过 Number.MAX_SAFE_INTEGER 和 Number.MIN_SAFE_INTEGER 的数值计算,且这个数值跟服务端语言 Java 的 Long 类型不一样且不兼容,所以实际开发中没有那么常用。想了解的可以戳 BigInt 这个链接
symbol 类型是一个原始值类型,于 ES2015 中引入,它与前面介绍到的 undefined/null/boolean/number/srting/bigint 这些基本不同的是,它没有包装类型。
symbol 类型的值在日常业务开发中几乎不会被使用到,是 ECMAScript 规范中特有的数据类型,它的存在让 ECMAScript 规范的语言具有 元编程(Metaprogramming) 的能力。
Symbol() 不是一个构造函数,而是用来返回 symbol 类型值的函数,且 Symbol() 的每一个返回值都是不相同的,即使传入了相同的参数。
const s1 = Symbol(1);
const s2 = Symbol(2);
console.log("s1等于s2:", s1 == s2); // s1等于s2: false
console.log("s1严格等于s2:", s1 === s2); // s1严格等于s2: false
2.4 object
object 类型是 ECMAScript 规范中唯一一种引用值类型,其他所有的引用值最终都是指向它的,包括上面几个小节提到的那些原始值类型的包装类型,最终都是继承自 Object 这个对象。
在 JavaScript 中,创建 object 类型的变量有两种方式:
- 方式一:
Object构造函数 - 方式二:对象字面量
// Object 构造函数创建对象变量
const o1 = new Object();
o1.count = 0;
// 对象字面量
const o2 = {
count: 0,
};
这两种方式创建的变量,它们最终都继承自 Object,都是 Object 类型的实例。
object 类型本身没有多少复杂的内容,只需要记住以下几点:
Object构造函数是所有引用值类型的起始类型,Object构造函数原型的原型是nullObject的原型方法,是所有引用值都可以访问到的- 所有对象字面量的构造函数都是
Object,这些字面量的__proto__属性都指向了Object.prototype
3. 类型判定
3.1 typeof
typeof 在 JavaScript 中是一个比较常用的类型判定关键词,其返回值为特定的字符串,针对不同的数据类型,返回值如下:
console.log(typeof null); // "object"
console.log(typeof undefined); // "undefined"
console.log(typeof true); // "boolean"
console.log(typeof 1); // "number"
console.log(typeof "a"); // "string"
console.log(typeof Symbol(2)); // "symbol"
console.log(typeof 1n); // "bigint"
console.log(typeof { count: 0 }); // "object"
console.log(typeof []); // "object"
function f1() {}
console.log(typeof f1); // "function"
为什么
typeof null会是"object"?这是一个历史遗留问题。
从上面的结果可以看出来,typeof 可以准确判定除了 null 以外的所有原始值类型和函数类型。
3.2 instanceof
instanceof 用于判断引用类型的关系,语法为 source instance target,返回值类型为 boolean。
console.log([] instanceof Array); // true
console.log([] instanceof Object); // true
console.log("" instanceof String); // false
3.3 Object.prototype.toString.call()
这是类型判定最精准的方法,这个方法的原理,可以参阅文章 关于Object.prototype.toString.call()。
console.log(Object.prototype.toString.call(undefined)); // '[object Undefined]'
console.log(Object.prototype.toString.call(null)); // '[object Null]'
console.log(Object.prototype.toString.call(false)); // '[object Boolean]'
基于上面的输出,我们可以封装一个函数来专门获取某个变量的值类型:
function getVarType(target) {
const typeString = Object.prototype.toString.call(target);
// 关键词的截取规则也可以采用其他的,这只是我的实现方式
return typeString.substring(7, typeString.length).replace("]", "").toLowerCase();
}
总结
本文较详细地介绍了 ECMAScript 规范和 JavaScript 中的变量、数据类型以及数据类型的判定。本文有的地方用的是 ECMAScript,有的地方用的是 JavaScript,之所以这样描述,是因为 ECMAScript 是规范、纲领,而 JavaScript 是规范的一种实现。实现可以不严格遵守规范,规范也可能被多门编程语言遵循。
本文如有描述错误的地方,欢迎指正~