js 这个语言从设计上看,是比较精简的,例如作用域,js 中理论上只有4中作用域:
- 块级作用域: let / const 工作在这里, 还有 TDZ (暂时性时间死区)
- 函数级作用域: var 工作在这个作用域 / 全局作用域
- 模块级作用域:ES6 引入,默认模块内的变量都是私有的
- 全局作用域: 例如浏览器环境下的 window
var & let & TDZ & 类型提升
之前写过类型提升相关文章,理解的不是很到位。事实上都会进行类型提升。但有一些细微区别。
var 的函数级作用域提升
- var 声明的变量,只工作在函数级作用域 > 模块作用域 (es6) > 全局作用域,不能工作在块级作用域
- 提升(hoisting): 就是把变量放到作用域最前边
- 同时为初始化的变量,会赋一个默认值 undedined
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:
{
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
这两种类型的函数主要区别:
- 箭头函数
- Function 函数,特有的 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 属性,也就不能作为构造函数使用。