js 这个语言从设计上看,是比较精简的,例如作用域,js 中理论上只有4中作用域:
- 块级作用域: let / const 工作在这里, 还有 TDZ (暂时性时间死区)
- 函数级作用域: var 工作在这个作用域 / 全局作用域
- 模块级作用域:ES6 引入,默认模块内的变量都是私有的
- 全局作用域: 例如浏览器环境下的 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
这两种类型的函数主要区别:
- 箭头函数
- 事实上没有自己的 this,是在定义的时候(创建时)可以捕获一个外部的this,后续不可修改。
- 不能使用 bind 函数,修改 this 的绑定,因为这个 this 是捕获来的 (readonly,常量指针)。
- Function 函数,特有的 this,动态绑定
- 有自己的 this,在运行时绑定,谁调用,this 就是谁。
- 可以通过 bind 函数修改 this 绑定的对象。
- 普通的标量对象也没有 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 只能指向全局作用域。
其他箭头函数和普通函数区别

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