ES6 块级作用域

ES6 的块级作用域,与 ES5 的区别在哪?如何创建,又有什么作用,解决了那些在 ES5 中遗留的问题?

ES5 中的作用域

  • function 作用域(由 function 声明的作用域,包括函数表达式、函数声明或是用 Function 所创建对象)
// 函数表达式
function fun1 (){
    var test = "test";
    return test;
}
// 函数声明
var fun2 = function () {
    var test = "test";
    return test;
}

console.log(test);
// ReferenceError: test is not defined

console.log(fun1());
// "test"
// test 在 fun 这个函数的作用域内才能被访问
  • 对象级别的作用域(使用 JavaScript 对象生成作用域)
var obj = {
    test: "test",
    method() {
        return this.test;
    }
}

console.log(test);
// ReferenceError: test is not defined

console.log(obj.method());
// "test"
// test 在 obj 这个对象的作用域内才能被访问

总的来说,ES5 中作用域依附于对象或函数存在,如果需要创建一个单独作用域,那么必须创建一个对象或函数。

但是会有很多人疑问:在 ES5 中像 if\for\while\do...while\switch 等也会有 {} 这不能创建一个作用域吗?在严格模式下这里面的声明变量是不允许的(通俗的讲就是错的),而目前绝大多数的浏览器都是可以用的原因,只是浏览器放宽了要求罢了。

大概描述了下 ES5 中的作用域,那就该介绍了主角了:块级作用域。

块级作用域

在具体的描述之前,想象一个场景:有一个数组,遍历这个数组,取得数组的值,然后根据该值有一个异步的调用。代码如下:

var arr = ["a", "b", "c"];

for(var i = 0, len = arr.length; i<len; i++){
    setTimeout(function() {
        console.log(arr[i]);
    }, 500);
}

看到这里,一定会有人说代码写错了,当然如果你觉得没错,可以打开浏览器试试,看看输出什么结果。由于异步调用并不会立即执行,并且 {} 并不能保证一个独立的作用域,因此当异步执行时,i 的值为 3arr[3] 就为 undefined 。正确的代码如下:

var arr = ["a", "b", "c"];

for(var i = 0, len = arr.length; i<len; i++) {
    // 用一个立即执行的函数保存 i 的值
    (function(index) {
        setTimeout(function() {
            console.log(arr[index]);
        },500);
    })(i)
}
// console: "a" "b" "c"

// 或者
var arr = ["a", "b", "c"];

for(var i = 0, len = arr.length; i<len; i++) {
    // 使用 setTimeout 保存 i 的值
    setTimeout(function(index) {
        console.log(arr[index]);
    }, 500, i);
}

我们不得不实现一个独立的作用域来锁住 i 的值。因此我们不能在异步函数中直接使用 for 循环中的 i

那么在了解了 ES5 中作用的局限后,来看看 ES6 中,如何实现上述的效果:

var arr = ["a", "b", "c"];

let len = arr.length;
for(let i = 0; i<len; i++){
    setTimeout(function() {
        console.log(arr[i]);
    }, 500);
}
// console: "a" "b" "c"

出人意料的简单!一样的代码,仅有的区别在于:ES5 的代码中变量是用 var 声明,而在 ES6 中,使用 let 声明。就像是 let 关键字 住了 i 的值,产生了 {} 级别的作用域。如下所示:

{
    let letTest = "test";
    var varTest = "test";
}

letTest     // ReferenceError: letTest is not defined.
varTest     // 2

ES6 新增了 let 命令,用于变量声明。用法类似于 var ,但是 let 声明的变量只在 let 所在的 {} 内有效。

文章刚开始的例子就能说明这个问题,由于 let 声明的变量只在每一个 for 循环的 {} 中有效,而 var 声明的变量由于 声明在 for 循环外,循环内是用的是同一个 var 变量,导致意外的结果。

let

let 变量的一些特点。

不存在变量提升

let 不会像 var 发生变量提升,所以变量的使用一定要在声明之后。

console.log(foo);   // 由于变量提升,输出undefined
console.log(bar);   // 不存在变量提升,报错 ReferenceError

var foo = "test";
let bar = "test";

暂时性死区(temporal dead zone,简称TDZ)

只要块级作用域内存在 let 命令,即使外部有该变量的声明,也不会有效。

var tmp = 123;

if (true) {
    tmp = 'abc';    // ReferenceError 暂时性死区导致使用不了外部的 tmp 变量
    let tmp;
}

ES6 明确规定,如果区块中存在 letconst 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

typeof 常被我们用来判断变量的类型,由于 var 声明会导致变量提升,因此在 let 出现之前, typeof 可以认为是一个不会报错的行为(最多被判定为 undefined)。但如果在 let 变量声明前使用 typeof 判断变量,就会报错(暂时性死区导致引用报错)。

但是有一点比较独特:假设一个变量根本就没声明呢?

typeof undeclared_variable  // "undefined"

反而是 undefined 了,感觉变量默认都是 var 声明的样子??这点之后说明。

一个比较隐蔽的 TDZ

// 由于默认值导致的 TDZ
function bar(x = y, y = 2) {
    return [x, y]
}

bar()               // 报错

// 等价于
{
    let x = y       // ReferenceError
    let y = 2
}
// 由于 y 声明前的区域为 TDZ,不能引用 y,因此报错。

// 这样是 OK 的
function bar(x = 2, y = x) {
    return [x, y]
}

bar()
// [2, 2]

注: 参数括号中的写法是 ES6 中给函数参数设置默认值的方式,= 之后的就为默认值。ES6 中参数的声明方式为 let,会导致 TDZ 的出现。

总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

虽然 TDZ 会导致各种引用错误,但 ES6 对于 TDZ 的规定,主要是为了减少运行时错误,导致意外的行为。

不允许重复声明

// 报错:重复声明
function repeat() {
    let test = "test1";
    var test = "test2";
}

// 报错:重复声明
function repeat() {
    let test = "test1";
    let test = "test2";
}

// 报错:变量名与参数名一致
function func(arg) {
    let arg
}

function func(arg) {
    {
        // 不报错 一个新的作用域
        let arg
    }
}

块级作用域的作用

ES5 中只有全局作用和函数/对象作用域,会带来很多问题。如下:

内层变量覆盖外层变量

var tmp = new Date();

function func() {
    console.log(tmp);
    if (false) {
        var tmp = "hello world";
    }
}

func() // undefined

由于变量的提升,覆盖了外层变量,即使 var tmp = "hello world" 永远不会被执行到。

用来循环计数的变量泄露

var str = 'hello';

for (var i = 0; i < str.length; i++) {
  console.log(str[i]);
}

console.log(i);         // 5

i 的功能其实只是计数而已,在 for 循环外应该不可见,但是它泄露了,而使用 let 就可以避免这个问题。

块级作用域与函数声明

讨论完了 letvar ,还有一个在 JavaScript 中很常见的东西: function !!!这就比较复杂了。
首先函数有两种形式:

函数声明语句

function func() {
    return "demo"
}

函数表达式

let func = function func() {
    return "demo"
}

第二种形式在这里不过多的讨论,因为是赋值形式,所以它遵循的 letvar 的区别,在这里也推荐尽量使用函数表达式的形式来书写函数。

下面就来好好讨论下函数声明语句,ES5 规定,函数只能在顶层作用域或函数作用域之中声明,不能在块级作用域声明。以下的写法都是非法的。虽然浏览器不会报错,但在严格模式下依然会报错。

// 情况一
if (true) {
    function func() {}
}

// 情况二
try {
    function func() {}
} catch(e) {
    ...
}

ES6 引入了块级作用域后,明确允许了在块级作用域之中声明函数。并且 ES6 规定:在块级作用域之中,函数声明语句的行为类似于 let ,在块级作用域之外不可引用。

以下代码显示了 ES5ES6 的区别:

function func() {
    console.log('I am outside!');
}

(function () {
    if (false) {
        // 重复声明一次函数 func
        function func() {
            console.log('I am inside!')
        }
    }
    f()
}());

// ES5 中就和以下代码一样
function f() {
    console.log('I am outside!')
}

(function () {
    function f() {
        console.log('I am inside!')
    }
    if (false) {
    }
    f();
}());
// console: I am inside!

// 而在 ES6 中则是这样
function f() {
    console.log('I am outside!')
}

(function () {
  f();
}());
// console: I am outside!

很显然,这种行为差异会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6 在附录B里面规定:浏览器的实现可以不遵守上面的规定,有自己的行为方式。

主要允许的行为为:

  • 允许在块级作用域内声明函数。
  • 函数声明类似于 var ,即会提升到全局作用域或函数作用域的头部。
  • 同时,函数声明还会提升到所在的块级作用域的头部。

注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作 let 处理。

const命令

const 声明一个只读的变量。一旦声明,变量的值就不能改变。和 let 命令类似,唯一不同的是 const 命令不能更改变量的值。同时,这也意味着, const 变量一旦声明,就必须立即初始化,不能留到以后赋值。

const foo
// SyntaxError: Missing initializer in const declaration

const 声明的常量,也与 let 一样不可重复声明。

var message = "Hello!"
let age = 25

// 以下两行都会报错
const message = "Goodbye!"
const age = 30

对于复合类型的变量,变量存的不是数据,而是指向数据所在的地址。所以只要保证该地址的数据不变,是可以更改对象内的内容的。这就好比你有一瓶雪碧,这瓶雪碧里有多少的量,它都只属于你。

const arr = [];
arr.push('Hello');      // 可执行
arr.length = 0;         // 可执行
arr = ['Dave'];         // 报错,内存地址发生变化

全局对象的属性

全局对象是最顶层的对象,在浏览器环境指的是 window 对象,在 Node.js 指的是 global 对象。 ES5 之中,全局对象的属性与全局变量是等价的,用 var 声明的变量,未声明直接使用的变量,都是全局对象下的一个属性。而在 ES6 中,为了保持兼容性, var 指令和 function 指令声明的变量,依旧是全局对象的属性,而 let 指令、 const 指令、 class 指令声明的变量,不属于全局对象的属性。

var test1 = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.test1;       // 1

let test2 = 2;
window.test2;       // undefined

这貌似能解决上面遗留的问题:

typeof undeclared_variable // "undefined"

这种形式下为什么是 undefined 了,全局对象下的一个属性,没有当然是 undefined 了。

Read more

Gitlab 搭建

Gitlab 搭建

为什么? 想要自己搭建一个代码仓库无非是以下几点原因: 1. 公司内部项目 2. 自己的项目,但不适合在公网 3. 大部分的 git 仓库虽然有私有服务,但价格都不便宜,甚至不如一台云服务器来的便宜 配置及安装文档 Gitlab * 由于 gitlab 会用到 22 端口端口转发的化就走不了 git clone 的默认配置,且占用内存较高,不推荐使用 docker 进行部署; * 由于 gitlab 自带 nginx 默认情况下会与属主机的 nginx 冲突,因此推荐只使用 gitlab 自带的 nginx 进行端口转发; 最小化配置 # path /etc/gitlab/gitlab.rb external_url 'http://git.

By breeze
NPM 私服

NPM 私服

verdaccio 私服搭建,搭建过程中的一些问题以及解决: * docker compose 启动后,可能会报错:配置找不到,添加 config.yaml 文件到映射的 ./conf 目录即可,config.yaml 具体内容可在官网找到,下方也有最小化配置。 services: verdaccio: image: verdaccio/verdaccio container_name: 'verdaccio' networks: - node-network environment: - VERDACCIO_PORT=4873 - VERDACCIO_PUBLIC_URL=http://npm.demo.com ports: - '10001:4873'

By breeze