ES5 只有两种声明变量的方法:var命令和function命令。ES6 添加let和const命令,import命令和class命令。所以,ES6 一共有 6 种声明变量的方法。
==1.function 命令==
基本用法
function声明变量即函数名,function是默认挂载在window下的
function fn(){};
console.log(window.fn) // ƒ fn(){}
变量提升
我们不论在全局作用域中或还是在局部作用域中,使用function都会被提升到该作用域的最顶部,这就是我们常说的变量提升
console.log(fn) // ƒ fn(){}
function fn(){}
fn没有被声明却输出了函数,说明fn被变量提升到了作用域顶层
函数作用域
函数作用域即局部作用域,函数作用域中的变量,在外面是不可以访问的。
function fn(){
var x=0
}
fn()
console.log(x) // x is not defined
这里var 声明的x只能在局部作用域中使用,他只会提升到局部作用域的顶部
==2.var 命令==
基本用法
在ES6之前都是通过var命令来声明变量,在全局作用域下使用var声明一个变量。默认它是挂载在顶层对象window下的。
var x=1;
console.log(x) // 1
console.log(window.x) // 1
用 var 声明的变量的作用域是它当前的执行上下文,可以是函数也可以是全局。
var x = 1 // 声明在全局作用域下
function fn() {
var x = 2 // 声明在 fn 函数作用域下
console.log(x) // 2
}
fn()
console.log(x) // 1
如果在 fn 没有声明 x ,而是赋值,则赋值的是 fn 外层作用域下的 x 。
var x = 1 // 声明在全局作用域下
function fn() {
x = 2 // 赋值
console.log(x) // 2
}
fn()
console.log(x) // 2
如果赋值给未声明的变量,该变量会被隐式地创建为全局变量(它将成为顶层对象的属性)。
a = 2
console.log(window.a) // 2
function fn(){
b = 3
}
fn()
console.log(window.b) // 3
变量提升
我们在全局作用域中或还是在局部作用域中,使用var关键字声明的变量,都会被提升到该作用域的最顶部,这就是我们常说的变量提升。
console.log(a) // undefined
var a = 1
// 提升的仅仅是变量声明,不会影响值的初始化,可以理解为:
var a
console.log(a) // undefined
a = 1
作用域
var声明可以在全局作用域的任何位置被访问,对包含它的代码块没有任何影响,而且多次声明同一个变量并不会报错。
function fn(){
var x=0;
for(var i=0;i<10;i++){
var x=1;
console.log(x,i); // 1,0 1,1 1,2 1,3 1,4 1,5 1,6 1,7 1,8 1,9
for(var i=0 ;i<10 ;i++){
console.log(i) // 1,0 0 1 2 3 4 5 6 7 8 9
}
}
}
fn()
fn函数中里层的for循环中的i覆盖了外层的i,导致外层循环只走了一次,第二次的时候已经i=10了。
变量重复命名和提升的经典案例
var fn=[];
for(var i=0;i<10;i++;){
fn[i]=function(){
console.log(i);
}
}
fn[6]() // 10
i是全局变量,后面的i值会覆盖前面的i值,for循环结束是i=10,之后引用的a[i],不论i为几,都是10。
缺点
所有未声明的变量和赋值的变量都会挂载在顶层对象下,造成全局环境变量污染; 多次声明同一变量而不报错,出现bug,导致代码不好维护。
==3.let 命令==
基本用法
ES6 新增了let命令,它的用法类似于var,但是let所声明的变量,只在当前代码块内有效。
{
let x = 1;
var y = 2;
}
x // ReferenceError: x is not defined.
y // 2
for循环,就很合适使用let命令。
var fn=[];
for(let i=0;i<10;i++;){
fn[i]=function(){
console.log(i);
}
}
fn[6]() // 6
使用let,声明的变量仅在块级作用域内有效,最后输出的是 6。
另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
上面代码表明函数内部的变量i与循环变量i不在同一个作用域(同一个作用域不可使用 let 重复声明同一个变量)。
不存在变量提升
var命令会发生“变量提升”。按照一般的逻辑,变量应该在声明语句之后才可以使用。为了纠正这种现象,let命令所声明的变量一定要在声明后使用,否则报错。
// var 的情况
console.log(x); // 输出undefined
var x= 2;
// let 的情况
console.log(y); // 报错ReferenceError
let y= 2;
暂时性死区
只要块级作用域内存在let命令,它所声明的变量就只在这个块级作用域有效,不再受外部的影响。
var x= 1;
if (true) {
x= 'a'; // ReferenceError
let x;
}
上面代码表示,即使有全局变量x,但是在块级作用域中用let声明x,会导致x只在这个块级作用域生效,所以给x赋值报错
ES6规定,如果块级作用域中有let或const命令,这个声明的变量只能在这个闭合作用域中使用。凡是在声明之前使用的变量,就会报错
在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
if (true) {
x= '1'; // ReferenceError
console.log(x); // ReferenceError
let x;
console.log(x); // undefined
x = 1;
console.log(x); // 1
}
上面代码中,在let命令声明变量x之前,都属于变量x的“死区”。
“暂时性死区”也意味着typeof不再是一个百分之百安全的操作。
typeof x; // ReferenceError
let x;
变量x使用let命令声明,所以在声明之前,都属于x的“死区”,只要用到该变量就会报错,反而如果一个变量根本没有被声明,使用typeof不会报错。
typeof x // "undefined"
不允许重复声明
let不允许在相同作用域内,重复声明同一个变量。
// 报错
function fn() {
let x = 1;
var x = 2;
}
// 报错
function fn() {
let x = 1;
let x = 2;
}
函数中有一些比较隐晦的重复声明
function fn(x) {
let x;
}
fn() // 报错
function fn(x) {
{
let x;
}
}
fn() // 不报错
==4.const 命令==
基本用法
const声明一个只读的常量。一旦声明,常量的值就不能改变。
const x = 3.1415;
x // 3.1415
x = 3;
// TypeError: Assignment to constant variable.
const声明的变量不能修改值,所以,const一旦声明变量,就必须立即初始化,不能之后赋值。
const x;
// SyntaxError: Missing initializer in const declaration
对于const来说,只声明不赋值,就会报错。
const的作用域与let命令相同:只在声明所在的块级作用域内有效。
if (true) {
const x = 1;
}
x // Uncaught ReferenceError: x is not defined
const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。
if (true) {
console.log(x); // ReferenceError
const x = 5;
}
const声明的常量,也与let一样不可重复声明。
var x= 1;
let y= 2;
// 以下两行都会报错
const x= 3;
const y= 4;
本质
const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
const obj = {
x:1,
};
obj.x = 2;
obj.x // 2
obj = {}; // Assignment to constant variable
const arr = [];
arr.push('1'); // 可执行
arr.length ; // 1
arr = [2]; // Assignment to constant variable.
上面代码中,常量arr是一个数组,常量obj是一个对象,这个数组/对象本身是可写的,但是如果将另一个数组/对象赋值给arr/obj,就会报错。
如果真的想将对象冻结,应该使用Object.freeze方法。
const obj = Object.freeze({});
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
obj.prop = 1;
上面代码中,常量obj指向一个被冻结的对象,所以添加新属性不起作用,严格模式时还会报错。
Object.freeze 函数执行下面的操作:
· 使对象不可扩展,这样便无法向其添加新属性。
· 为对象的所有属性将 configurable 特性设置为 false。在 configurable 为 false 时,无法更改属性的特性且无法删除属性。
· 为对象的所有数据属性将 writable 特性设置为 false。当writable 为false时,无法更改数据属性值。
除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
==5.import 命令==
基本用法
import用于加载文件,在大括号接收的是一个或多个变量名,这些变量名需要与你想要导入的变量名相同。
import { Button } from 'example'
上面代码,你就从example.js中获取到了一个叫 Button 的变量,可以对Button里的代码进行操作。
如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。
import { NewButton as Button } from 'example';
import命令输入的变量都是只读的。
import { Button } from 'example'
Button=1 // Assignment to constant variable.
import是静态执行,不能使用表达式和变量。
// 报错
import { 'Bu' + 'tton' } from 'example'
// 报错
let module = 'example'
import { Button } from module
// 报错
if (x === 1) {
import { Button } from 'example'
} else {
import { Button } from 'example'
}
import 命令具有提升效果,会提升到整个模块的头部,首先执行。
Button()
import { Button } from 'example'
规范
在目前所有的js引擎中,import并没有被完全实现,通常所说的支持ES6其实是转码为ES5再执行,import语法会被转码为require。
这也是为什么,导出时使用module.exports,在引入此模块时使用import仍然起效,因为本质上,import会被转码为require去执行。
==6.class 命令==
类的由来
javaScript语言中,生成实例对象的传统方法是通过构造函数。
function Example(x, y) {
this.x = x;
this.y = y;
}
Example.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Example(1, 2);
ES6中则是引入了Class(类)的概念,作为对象的模板。通过Class关键字定义类。
class Example{
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var p = new Example(1,2)
ES6 的类,完全可以看作构造函数的另一种写法
class Example{
// ...
}
typeof Example// "function"
Example=== Example.prototype.constructor // true
注: · 类中方法与方法之间不需要逗号分隔,加了会报错。 · 类的数据类型就是函数,类本身就指向构造函数。
由于类的方法都定义在prototype对象上面,所以类的新方法可以添加在prototype对象上面。Object.assign()方法可以很方便地一次向类添加多个方法。
class Example{
constructor(){
// ...
}
}
Object.assign(Example.prototype, {
toString(){},
toValue(){}
});
类的内部所有定义的方法,都是不可枚举的。
class Example{
constructor(x, y) {
// ...
}
toString() {
// ...
}
}
Object.keys(Example.prototype)
// []
Object.getOwnPropertyNames(Example.prototype)
// ["constructor","toString"]
上面代码中,toString()方法是Example类内部定义的方法,它是不可枚举的。这一点与 ES5 的行为不一致。
枚举是指对象中的属性是否可以被遍历。 JavaScript对象的属性可分为可枚举和不可枚举,它是由属性的enumeration值决定的,true为可枚举,false为不可枚举 1)for...in
可遍历原型与实例上的所有可枚举属性
2)Object.keys();
只能返回对象本身具有的可枚举属性。
3)JSON.stringify();
只能读取对象本身的可枚举属性,并序列化为JSON对象。
4)Object.getOwnPropertyNames()
遍历自身所有属性(不论是否是可枚举的),不包括原型链上面的.。
5)Object.assign() (es6新增)
自身的可枚举属性。
var Example= function (x, y) {
// ...
};
Example.prototype.toString = function () {
// ...
};
Object.keys(Example.prototype)
// ["toString"]
Object.getOwnPropertyNames(Example.prototype)
// ["constructor","toString"]
上面代码是ES5写法,toString()方法是可以遍历出来的,是可枚举的。
constructor()方法
constructor()方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加。
类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。
class Example{
}
// 等同于
class Example{
constructor() {}
}
Example()
// TypeError: Class constructor Foo cannot be invoked without 'new'
类的实例
与 ES5 一样,类的所有实例共享一个原型对象。
class Example{
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var p1 = new Example(2,3);
var p2 = new Example(3,2);
p1.__proto__ === p2.__proto__
//true
上面代码中,p1和p2都是Point的实例,它们的原型都是Point.prototype,所以__proto__属性是相等的。
这也意味着,可以通过实例的__proto__属性为“类”添加方法。
__proto__并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用Object.getPrototypeOf()方法来获取实例对象的原型,然后再来为原型添加方法/属性。
var x1 = new Example(2,3);
var x2 = new Example(3,2);
x1.__proto__.printName = function () { return 'a' };
x1.printName() // "a"
x2.printName() // "a"
var x3 = new Example(4,2);
x3.printName() // "a"
上面代码在x1的原型上添加了一个printName()方法,由于x1的原型就是x2的原型,因此x2也可以调用这个方法。而且,此后新建的实例x3也可以调用这个方法。这意味着,使用实例的__proto__属性改写原型,必须相当谨慎,不推荐使用,因为这会改变“类”的原始定义,影响到所有实例。
实例属性的新写法
ES2022为类的实例属性,又规定了一种新写法。实例属性现在除了可以定义在constructor()方法里面的this上面,也可以定义在类内部的最顶层。
class Example{
x= 0;
constructor() {
this.y= 10;
}
get value() {
return this.x+this.y;
}
numAdd() {
this.x++;
this.y++;
}
}
注意,新写法定义的属性是实例对象自身的属性,而不是定义在实例对象的原型上面。
取值函数(getter)和存值函数(setter)
与 ES5 一样,在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
class Example{
x=0;
get prop() {
return this.x;
}
set prop(value) {
this.x=value
}
}
let Example= new Example();
Example.prop = 2;
// 2
Example.prop
// 0
属性表达式
类的属性名,可以采用表达式。
let methodName = 'toString';
class Example{
[methodName]() {
}
}
Class表达式
与函数一样,类也可以使用表达式的形式定义
const MyExample = class Example {
getClassName(){
return Example.name;
}
};
let warp = new MyExample();
warp.getClassName(); // Example
上面这个类的名字是Example,但是Example只在类的内部可以使用,在类的外部只能使用MyExample引用
类的内部如果没有用到Example可以省略,如
const MyExample = class {
getClassName(){
return Example.name;
}
}
静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
class Example {
static classMethod(){
return 'Method';
}
}
Example.classMethod() // Method
静态属性
静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。
class Example {
}
Example.prop = a;
Example.prop // a
类的注意点
1.严格模式
2.不存在提升
3.name属性
4.this指向
class继承
Class 可以通过extends关键字实现继承,让子类继承父类的属性和方法。extends 的写法比 ES5 的原型链继承,要清晰和方便很多。
参考资料:ES6入门文档