XiaoboTalk

JS 作用域 & 相关设计

js 这个语言从设计上看,是比较精简的,例如作用域,js 中理论上只有4中作用域:
  1. 块级作用域: let / const 工作在这里, 还有 TDZ (暂时性时间死区)
  1. 函数级作用域: var 工作在这个作用域 / 全局作用域
  1. 模块级作用域:ES6 引入,默认模块内的变量都是私有的
  1. 全局作用域: 例如浏览器环境下的 window

var & let & TDZ & 类型提升

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

var 的函数级作用域提升

  • var 声明的变量,只工作在函数级作用域 > 模块作用域 (es6) > 全局作用域,不能工作在块级作用域
  • 提升(hoisting): 就是把变量放到作用域最前边
  • 同时为初始化的变量,会赋一个默认值 undedined
    • undefined 是一个 js 类型,这个类型集合中只有一个值就是 undefined
    • 因为默认赋值了 undefined ,所以 typeof 关键字在这样的上下文里是安全的
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
    • 所以在 typeof 去访问一个未初始化的 let 或者 const 会报错
    • 这个报错只在块级作用域有效,也称为 TDZ: 临时性时间死区
    • 块级作用域也会进行遮蔽初始化,(临近原则)
示例1:
{ let foo = 'local'; // 只在块级作用域,外边无法访问 } console.log(foo); // ReferenceError: foo is not defined 报错
示例2,未初始化的访问 TDZ (注意:未初始化报错,不是未定义的报错):
{ 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:
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
console.log('before foo: ', foo); // undefined { var foo; } // 翻译 代码: var foo = undefined; console.log('before foo: ', foo); // undefined { }
 

Arrow Function 和 Function

这两种类型的函数主要区别:
  1. 箭头函数
    1. 事实上没有自己的 this,是在定义的时候(创建时)可以捕获一个外部的this,后续不可修改。
    2. 不能使用 bind 函数,修改 this 的绑定,因为这个 this 是捕获来的 (readonly,常量指针)。
  1. Function 函数,特有的 this,动态绑定
    1. 有自己的 this,在运行时绑定,谁调用,this 就是谁。
    2. 可以通过 bind 函数修改 this 绑定的对象。
    3. 普通的标量对象也没有 this,只有在函数上下文中,才会有 this,并且默认绑定到当前调用者对象。
 
总结
┌────────────────────────────┐ │ JavaScript 中的 this │ ├────────────────────────────┤ │ 普通函数(function) │ │ ✔ 有自己的 this │ │ ✔ 调用时决定谁是 this │ ├────────────────────────────┤ │ 箭头函数(=>) │ │ ✘ 没有自己的 this │ │ ✔ 定义时捕获外层作用域的 this │ └────────────────────────────┘
 
示例1:
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,通常用来隐藏内部实现,只返回一个接口对象,例如:
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,会绑定到全局作用域

const obj = { // 普通的标量对象,没有自己的 this name: '绑定测试 obj', say: () => { console.log(this.name); // 注意,这个 this,是从外部的全局作用域捕获来的。 } }; obj.say(); // 输出 undefined,因为全局作用域没有 name 这个属性, // 这里 this 指向了全局作用域
注意:对象定义的 {} 内是不构成作用域的。所以 this 只能指向全局作用域。

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

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