ES6声明变量的六种方法

144 阅读8分钟

ES5 只有两种声明变量的方法:var命令和function命令。ES6 添加letconst命令,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。在 configurablefalse 时,无法更改属性的特性且无法删除属性。

· 为对象的所有数据属性将 writable 特性设置为 false。当writablefalse时,无法更改数据属性值。

除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。

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

上面代码中,p1p2都是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 一样,在“类”的内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

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入门文档