你不知道的JS(上):this指向与对象基础
本文是《你不知道的JavaScript(上卷)》的阅读笔记,第二部分:this 指向与对象基础。 供自己以后查漏补缺,也欢迎同道朋友交流学习。
动态作用域
JS 并不具有动态作用域,它只有词法作用域,但 this 机制在某种程度上很像动态作用域。
主要区别:
- 词法作用域 :在写代码或者说定义时确定的(静态),关注函数在何处声明。
- 动态作用域 :在运行时确定的,关注函数从何处调用 。
this也是在运行时绑定的,这一点与动态作用域类似。
this 词法
javascript
var obj = {
id: "awesome",
cool: function coolFn() {
console.log( this.id );
}
};
var id = "not awesome";
obj.cool(); // awesome
setTimeout( obj.cool, 100 ); // not awesome
关于 this
this 关键字是 JS 中最复杂的机制之一,它被自动定义在所有函数的作用域中。
对 this 的误解
1. 为什么要用 this?
this 提供了一种更优雅的方式来隐式"传递"一个对象引用,因此可以将 API 设计得更加简洁并且易于复用。随着使用模式越来越复杂,显式传递上下文对象会让代码变得混乱,而使用 this 则能保持代码整洁。
2. 它的作用域
this 在任何情况下都不指向函数的词法作用域。
javascript
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log(this.a);
}
foo(); // ReferenceError: a is not defined
this 到底是什么
this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录(也被称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的其中一个属性,会在函数执行的过程中被用到。
this 定义小结
this 既不指向函数自身,也不指向函数的词法作用域。this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
this 全面解析
调用位置
在理解 this 的绑定过程之前,首先要理解调用位置 :调用位置就是函数在代码中被调用的位置(而不是声明的位置)。只有仔细分析调用位置才能回答:这个 this 到底引用的是什么?
绑定规则
1. 默认绑定
独立函数调用:函数调用时应用了 this 的默认绑定,因此 this 指向了全局对象。
javascript
function foo () {
console.log(this.a);
}
var a = 2;
foo(); // 2
如果使用严格模式(strict mode),全局对象将无法使用默认绑定,因此 this 会绑定到 undefined。
2. 隐式绑定
这条规则需要考虑调用位置是否有上下文对象,或者说是否被某个对象拥有或包含。
javascript
function foo () {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
}
obj.foo(); // 2
当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。
对象属性引用链: 只有最顶层或者说最后一层会影响调用位置。
javascript
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
隐式丢失:
一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,从而应用默认绑定(绑定到全局对象或 undefined)。
javascript
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名,实际上引用的是 foo 函数本身
var a = "oops, global";
bar(); // "oops, global"
传入回调函数时也容易发生隐式丢失:
javascript
function doFoo(fn) {
fn(); // 调用位置!
}
doFoo(obj.foo); // "oops, global"
3. 显式绑定
JS 提供了 call(..) 和 apply(..) 两个方法来进行显式绑定。它们的第一个参数是一个对象,会把这个对象绑定到 this。
javascript
function foo () {
console.log(this.a);
}
var obj = { a: 2 };
foo.call(obj); // 2
装箱: 如果传入的是原始值(字符串、布尔或数字),它会被转换成对象形式(new String(..) 等)。
硬绑定:
javascript
function bind(fn, obj) {
return function() {
return fn.apply(obj, arguments);
}
}
4. new 绑定
使用 new 来调用函数时,会自动执行以下操作:
- 创建(构造)一个全新的对象。
- 这个新对象会被执行
[[Prototype]]连接。 - 这个新对象会绑定到函数调用的
this。 - 如果函数没有返回其他对象,那么
new表达式中的函数调用会自动返回这个新对象。
javascript
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2
优先级
- new 绑定 :
var bar = new foo() - 显式绑定 :
var bar = foo.call(obj2) - 隐式绑定 :
var bar = obj1.foo() - 默认绑定 :
var bar = foo()(严格模式下绑定到undefined)
绑定例外
被忽略的 this
如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值会被忽略,实际应用的是默认绑定规则。
更安全的 this: 使用 Object.create(null) 创建一个彻底的空对象(DMZ)。
javascript
var ø = Object.create(null);
foo.apply(ø, [2, 3]);
间接引用
赋值表达式 p.foo = o.foo 的返回值是目标函数的引用,调用时会应用默认绑定。
软绑定
硬绑定会降低函数的灵活性,软绑定可以在保留隐式/显式绑定能力的同时,提供一个默认绑定值。
this 词法(箭头函数)
箭头函数不使用 this 的四种标准规则,而是根据外层(函数或全局)作用域 来决定 this。
javascript
function foo() {
return (a) => {
// this 继承自 foo()
console.log( this.a );
};
}
this 绑定规则小结
判断 this 绑定对象的四条规则:
- new? 绑定到新创建的对象。
- call/apply/bind? 绑定到指定的对象。
- 上下文对象调用? 绑定到该上下文对象。
- 默认? 严格模式下
undefined,否则全局对象。
对象
语法
对象可以通过两种形式定义:字面量形式 和构造形式。
javascript
// 文字语法(常用)
var myObj = { key: value };
// 构造形式
var myObj = new Object();
myObj.key = value;
类型
JS 的六种主要类型:string、number、boolean、null、undefined、object。
注意 :
typeof null返回"object"是语言本身的一个 bug。
内置对象 :String、Number、Boolean、Object、Function、Array、Date、RegExp、Error。它们实际上都是内置函数。
内容
可计算属性名
ES6 允许在字面量中使用 [] 包裹表达式作为属性名。
javascript
var prefix = "foo";
var myObject = {
[prefix + "bar"]: "hello"
};
数组
数组也是对象,可以添加属性,但如果属性名看起来像数字,会变成数值下标并修改 length。
复制对象
- 深拷贝 :对于 JSON 安全的对象,可以使用
JSON.parse(JSON.stringify(obj))。 - 浅拷贝 :ES6 提供了
Object.assign(..)。
属性描述符 (Property Descriptors)
writable:是否可修改值。configurable:是否可配置(修改描述符或删除属性)。enumerable:是否出现在枚举中(如for..in)。
不变性
- 对象常量 :
writable:false+configurable:false。 - 禁止扩展 :
Object.preventExtensions(..)。 - 密封 :
Object.seal(..)(禁止扩展 +configurable:false)。 - 冻结 :
Object.freeze(..)(最高级别,密封 +writable:false)。
[[Get]] 与 [[Put]]
[[Get]]:查找属性值,找不到返回undefined。[[Put]]:设置属性值,涉及是否有 setter、是否可写等判断。
Getter 和 Setter
通过 get 和 set 改写默认的 [[Get]] 和 [[Put]] 操作。定义了 getter/setter 的属性被称为"访问描述符"。
存在性
in操作符:检查属性是否在对象及其原型链中。hasOwnProperty(..):只检查属性是否在对象自身中。
遍历
for..in:遍历对象的可枚举属性(包括原型链)。forEach(..)、every(..)、some(..):数组辅助迭代器。for..of(ES6):直接遍历值(通过迭代器对象)。
混合对象"类"
类是一种设计模式。JS 虽然有 class 关键字,但其机制与传统面向对象语言完全不同。
类的机制
- 实例化:类通过复制操作变为对象。
- 继承:子类继承父类。
- 多态 :子类重写父类方法,通过
super相对引用。
混入 (Mixin)
由于 JS 不会自动执行复制行为,开发者常使用"混入"来模拟类复制。
- 显式混入:手动复制属性。
- 寄生继承:显式混入的一种变体。
- 隐式混入 :通过
call(this)借用函数。
对象与类小结
类意味着复制。JS 中没有真正的类,只有对象,对象之间是通过关联(原型链)而非复制来连接的。