js 这个语言从设计上看,是比较精简的,例如作用域,js 中理论上只有4中作用域:

  1. 块级作用域: let / const 工作在这里, 还有 TDZ (暂时性时间死区)
  2. 函数级作用域: var 工作在这个作用域 / 全局作用域
  3. 模块级作用域:ES6 引入,默认模块内的变量都是私有的
  4. 全局作用域: 例如浏览器环境下的 window

var & let & TDZ & 类型提升

之前写过类型提升相关文章,理解的不是很到位。事实上都会进行类型提升。但有一些细微区别。

var 的函数级作用域提升

  • var 声明的变量,只工作在函数级作用域 > 模块作用域 (es6) > 全局作用域,不能工作在块级作用域
  • 提升(hoisting): 就是把变量放到作用域最前边
  • 同时为初始化的变量,会赋一个默认值 undedined
typescript
if (true) {
  let a = 1;
  const b = 2;
  var c = 3;
}
console.log(a); // ❌
console.log(b); // ❌
console.log(c); // ✅ 注意:var 泄漏到了外面

let / const 块级作用域

ES6 之后,let 和 const 可以工作在块级作用域下:例如上边的代码。特使有如下特点:

  • let const 也会被提升到块级作用域最前边
  • 未初始化的 let 或者 const 变量,不会被赋值默认的 undefined 。

示例1:

typescript
{
  let foo = 'local'; // 只在块级作用域,外边无法访问
}
console.log(foo); // ReferenceError: foo is not defined 报错

示例2,未初始化的访问 TDZ (注意:未初始化报错,不是未定义的报错):

typescript
{
    console.log(foo); // ReferenceError: Cannot access 'foo' before initialization
    let foo = 'local';
}

// 上边的代码翻译一下:
{
    let foo; // 提升 foo 定义,但是不进行初始化
    console.log(foo); // TDZ: 未初始化报错,不是未定义的报错: ReferenceError: Cannot access 'foo' before initialization
    foo = 'local';
}

示例3:

typescript
console.log('before foo: ', foo); // undefined
{
    var foo = 'local';
}
console.log('after foo: ', foo); // local

// 翻译代码:

var foo = undefined; // 提升并且初始化
console.log('before foo: ', foo); // undefined
{
	  foo = 'local';
}
console.log('after foo: ', foo); // local

示例4:即使 foo 没有在代码中被初始化,也会被 hoisting 初始化为 undefined

typescript
console.log('before foo: ', foo); // undefined 
{
    var foo;
}

// 翻译 代码:
var foo = undefined;
console.log('before foo: ', foo); // undefined 
{
}

Arrow Function 和 Function

这两种类型的函数主要区别:

  1. 箭头函数
  2. Function 函数,特有的 this,动态绑定

总结

typescript
┌────────────────────────────┐
│        JavaScript 中的 this        │
├────────────────────────────┤
│        普通函数(function)        │
│   ✔ 有自己的 this                 │
│   ✔ 调用时决定谁是 this            │
├────────────────────────────┤
│       箭头函数(=>)             │
│   ✘ 没有自己的 this              │
│   ✔ 定义时捕获外层作用域的 this   │
└────────────────────────────┘

示例1:

typescript
const obj = {
  name: '绑定测试 obj',
  say: function () {
    console.log(this.name);
  }
};

obj.say(); // 输出 '绑定测试 obj',因为是 obj 调用的 say 这个function

const obj1 = {
    name: 'obj1',
    say: obj.say,
}
obj1.say(); // 输出 obj1,因为是 obj1 调用的 function  say

const obj2 = {
  name: 'obj2',
  say: obj.say.bind(obj),
}
obj2.say(); // 输出: 绑定测试 obj, 因为 obj2 的say,被固定绑定了 this 到 obj 上

通过 bind 函数绑定一个固定的 this,通常用来隐藏内部实现,只返回一个接口对象,例如:

typescript
export const makeCSSRecordBox = (initial: CSSRecord = {}) : CSSRecordBox => {
    const context = {
        packer: initial,
        pack: function (cssRecord: PackFunctionParam): void {
            if ('key' in cssRecord && 'value' in cssRecord) {
                this.packer = {
                    ...this.packer,
                    ...{
                        [cssRecord.key]: cssRecord.value
                    },
                }
            } else {
                this.packer = {
                    ...this.packer,
                    ...cssRecord,
                }
            }
        },
        record: function (): CSSRecord {
            return this.packer
        }
    }
    return {
        pack: context.pack.bind(context),
        record: context.record.bind(context),
    }
}

/* 最后 return 的对象是:
return {
    pack: context.pack.bind(context),
    record: context.record.bind(context),
}

这个对象是没有 packer 这个属性的,外部如果使用这个对象,那么默认this指向了调用者,
也就是这个 {} 对象,会包 packer 找不到
使用 bind 固定绑定 this 到,context 上,外部调用的时候,pack 函数内部就能正常访问到 packer
同时,外部只看到 pack 和 record 两个函数,隐藏了实现细节。
同时这种写法也更加的函数式,没有副作用
*/

箭头函数无法在定义对象的时候绑定 this,会绑定到全局作用域

typescript
const obj = { // 普通的标量对象,没有自己的 this
  name: '绑定测试 obj',
  say: () => {
    console.log(this.name); // 注意,这个 this,是从外部的全局作用域捕获来的。
  }
};

obj.say(); // 输出 undefined,因为全局作用域没有 name 这个属性,
// 这里 this 指向了全局作用域

注意:对象定义的 {} 内是不构成作用域的。所以 this 只能指向全局作用域。

其他箭头函数和普通函数区别

其中有个略显多余,因为没有 prototype 属性,也就不能作为构造函数使用。