this 是什么
this
的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈) 、函数的调用方法、传入的参数等信息。
this
就是记录的其中一个属性,会在函数执行的过程中用到。
this 全面解析
调用位置
调用位置是函数在代码中被调用的位置,而不是声明的位置。
找调用位置最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的调用位置就在当前正在执行的函数的前一个调用中。
javascript
function baz() {
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域
console.log('baz');
bar(); // <-- bar的调用位置
}
function bar() {
// 当前调用栈是:baz -> bar
// 因此,当前调用位置在baz中
console.log('bar');
foo(); // <-- foo的调用位置
}
function foo() {
// 当前调用栈是:baz -> bar -> foo
// 因此,当前调用位置在bar中
console.log('foo');
}
baz(); // <-- baz的调用位置
绑定规则
默认绑定
默认绑定是无法应用其他规则时的默认规则。
在非严格模式下,独立调用函数(直接使用不带任何修饰的函数引用进行调用的函数)调用时会应用 this
的默认绑定,因此 this
指向全局对象。
如果使用严格模式,那么全局对象将无法使用默认绑定,因此 this
会绑定到 undefined
。
隐式绑定
隐式绑定需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。
javascript
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo,
};
obj.foo(); // 2
上述代码的调用位置会使用 obj
上下文来引用函数,也可以说函数被调用时 obj
对象"拥有"或者"包含"它。
当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this
绑定到这个上下文对象。
对象属性引用链中只有最顶层或者说最后一层会影响调用位置。举例来说:
javascript
function foo() {
console.log(this.a);
}
var obj2 = {
a: 42,
foo: foo,
};
var obj1 = {
a: 2,
obj2: obj2,
};
obj1.obj2.foo(); // 42
上述代码中,obj1.obj2.foo()
的调用位置会使用 obj2
作为函数的上下文,因此 this
绑定到 obj2
对象。
一个最常见的 this
绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,比如:
javascript
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo,
};
var bar = obj.foo; // 函数别名!
var a = 'oops, global'; // a是全局对象的属性
bar(); // "oops, global"
虽然 bar
是 obj.foo
的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的 bar
其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
javascript
function foo() {
console.log(this.a);
}
function doFoo(fn) {
// fn其实引用的是foo
fn(); // <-- 调用位置!
}
var obj = {
a: 2,
foo: foo,
};
var a = 'oops, global'; // a是全局对象的属性
doFoo(obj.foo); // "oops, global"
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以也会应用默认绑定。
显式绑定
可以使用函数的 call
和 apply
方法对 this
进行显式绑定。
如果给
call
和apply
方法传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作this
的绑定对象,这个原始值会被转换成它的对象形式(也就是new String(..)
、new Boolean(..)
或者new Number(..)
,这通常被称为"装箱"。
- 硬绑定:
javascript
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
// 简单的辅助绑定函数
function bind(fn, obj) {
return function () {
return fn.apply(obj, arguments);
};
}
var obj = {
a: 2,
};
var bar = bind(foo, obj);
var b = bar(3); // 2 3
console.log(b); // 5
由于硬绑定是一种非常常用的模式,所以在 ES5 中提供了内置的方法 Function.prototype.bind
,它的用法如下:
javascript
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
var obj = {
a: 2,
};
var bar = foo.bind(obj);
var b = bar(3); // 2 3
console.log(b); // 5
bind
会返回一个硬编码的新函数,并把参数设置为 this
的上下文。
- API 调用的"上下文"
第三方库的许多函数,以及 JavaScript
语言和宿主环境中许多新的内置函数,都提供了一 个可选的参数,通常被称为"上下文" (context
) ,其作用和 bind
一样,确保你的回调函数使用指定的 this
。
javascript
function foo(el) {
console.log(el, this.id);
}
var obj = {
id: 'awesome',
};
// 调用foo(..)时把this绑定到obj
[1, 2, 3].forEach(foo, obj);
// 1 awesome 2 awesome 3 awesome
这些函数实际上就是通过 call
或者 apply
实现了显式绑定。
new 绑定
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行[[原型]] 连接。
- 这个新对象会绑定到函数调用的 this。
- 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
javascript
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2
使用 new
来调用 foo
时,我们会构造一个新对象并把它绑定到 foo
调用中的 this
上。new
是最后一种可以影响函数调用时 this
绑定行为的方法,我们称之为 new
绑定。
优先级
我们可以通过以下顺序来判断 this
的绑定优先级:
-
函数是否在
new
中调用(new
绑定)?如果是的话this
绑定的是新创建的对象。 -
函数是否通过
call
、apply
(显式绑定)或者硬绑定调用?如果是的话,this
绑定的是 指定的对象。 -
函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,
this
绑定的是那个上下文对象。 -
如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到
undefined
,否则绑定到全局对象。
对象
语法
对象可以通过两种形式定义:声明(文字)形式和构造形式。
类型
在 JavaScript 中一共有六种主要类型(ES6 之前):
- string
- number
- boolean
- null
- undefined
- object
函数就是对象的一个子类型(从技术角度来说就是"可调用的对象")。
JavaScript
有一些内置对象:
- String
- Number
- Boolean
- Object
- Function
- Array
- Date
- RegExp
- Error
在字符串、数值和布尔字面量上访问属性或者方法时,引擎自动把字面量转换成对应的对象。
例如:
javascript
var str = 'hello';
console.log(str.length); // 'hello' => new String('hello')
var num = 123;
console.log(num.toFixed(2)); // '123' => new Number(123)
var bool = true;
console.log(bool.toString()); // 'true' => new Boolean(true)
null
和 undefined
没有对应的构造形式,它们只有文字形式。相反,Date
只有构造,没有文字形式。
对于 Object
、Array
、Function
和 RegExp
(正则表达式)来说,无论使用文字形式还是构造形式,它们都是对象,不是字面量。
Error
对象很少在代码中显式创建,一般是在抛出异常时被自动创建。也可以使用 new Error(..)
这种构造形式来创建。
内容
对象的内容是由一些存储在特定命名位置的(任意类型的)值组成的,我们称之为属性。
需要强调的一点是,在引擎内部,这些值的存储方式是多种多样的,一般并不会存在对象容器内部。存储在对象容器内部的是这些属性的名称,它们就像指针(从技术角度来说就是引用)一样,指向这些值真正的存储位置。
需要使用 .
操作符或者 []
操作符来访问对象的属性。.
语法通常被称为"属性访问",[]
语法通常被称为"键访问"。这两种语法的主要区别在于 .
操作符要求属性名满足标识符的命名规范,而 []
语法可以接受任意 UTF-8/Unicode
字符串作为属性名。
可计算属性名
ES6
增加了可计算属性名,可以在文字形式中使用 []
包裹一个表达式来当作属性名。
可计算属性名最常用的场景可能是 ES6 的符号(Symbol)。
javascript
var myObject = {
[Symbol.Something]: 'hello world',
};
属性描述符
javascript
var myObject = { a: 2 };
Object.getOwnPropertyDescriptor(myObject, 'a');
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
writable
:表示是否可以修改属性的值。如果设置为false
,对于属性值的修改静默失败,严格模式下会报TypeError
。configurable
:表示属性是否可配置。只要属性是可配置的,就可以使用defineProperty
方法来修改属性描述符,也可以对属性进行删除操作,如果设置为false
,则修改属性描述符会报TypeError
,并且删不掉该属性。enumerable
:表示属性是否可枚举。如果设置为false
,for...in
循环不会枚举这个属性。
不变性
- 对象常量
结合 writable:false
和 configurable:false
就可以创建一个真正的常量属性(不可修改、重定义或者删除)
- 禁止扩展
可以使用 Object.preventExtensions(obj)
方法来禁止扩展对象,一旦对象被禁止扩展,就不能再添加新属性。在严格模式下,禁止扩展之后尝试添加新属性会报 TypeError
。
- 密封
Object.seal(obj)
会创建一个"密封"的对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions(obj)
并把所有现有属性标记为 configurable:false
。 所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以 修改属性的值)。
- 冻结
Object.freeze(obj)
会创建一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal(obj)
并把所有"数据访问"属性标记为 writable:false
,这样就无法修改它们的值。
[[Get]]
当我们读取一个对象的属性时,引擎会调用 [[Get]]
操作。
对象默认的内置 [[Get]]
操作首先在对象中查找是否有名称相同的属性,如果找到就会返回这个属性的值。如果没有找到名称相同的属性,那么就会遍历可能存在的 [[Prototype]]
链,也就是原型链。
[[Put]]
如果已经存在这个属性,[[Put]]
算法大致会检查下面这些内容。
- 属性是否是访问描述符?如果是并且存在
setter
就调用setter
。 - 属性的数据描述符中
writable
是否是false
?如果是,在非严格模式下静默失败,在严格模式下抛出TypeError
异常。 - 如果都不是,将该值设置为属性的值。
如果对象中不存在这个属性,[[Put]]
操作会更加复杂。
Getter 和 Setter
可以使用 getter
和 setter
部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象上。
getter
是一个隐藏函数,会在获取属性值时调用。
setter
也是一个隐藏函数,会在设置属性值时调用。
javascript
var myObject = {
// 给 a 定义一个getter
get a() {
return this._a_;
},
// 给 a 定义一个setter
set a(val) {
this._a_ = val * 2;
},
};
myObject.a = 2;
myObject.a; // 4
存在性
in
: 操作符会检查某个属性名是否在对象及其 [[Prototype]]
原型链中 。 hasOwnProperty
:只会检查属性是否在对象中,不会检查 [[Prototype]]
原型链。
javascript
var myObject = {
a: 2,
};
'a' in myObject; // true
'b' in myObject; // false
myObject.hasOwnProperty('a'); // true
myObject.hasOwnProperty('b'); // false
判断属性是否可枚举:
- 不可枚举的属性不会出现在
for...in
循环中。 propertyIsEnumerable
会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足enumerable:true
。Object.keys
会返回一个数组,包含所有可枚举的属性名。
遍历
普通的对象没有内置的 @@iterator
,所以无法自动完成 for..of
遍历。
当然,你可以给任何想遍历的对象定义 @@iterator
,举例来说:
javascript
var myObject = {
a: 2,
b: 3,
};
Object.defineProperty(myObject, Symbol.iterator, {
enumerable: false,
writable: false,
configurable: true,
value: function () {
var o = this;
var idx = 0;
var ks = Object.keys(o);
return {
next: function () {
return {
value: o[ks[idx++]],
done: idx > ks.length,
};
},
};
},
});
// 手动遍历myObject
var it = myObject[Symbol.iterator]();
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { value:undefined, done:true }
// 用for..of遍历myObject
for (var v of myObject) {
console.log(v);
}
// 2
// 3
只要迭代器的 next()
调用会返回 { value: ... }
和 { done: ... }
,for..of
就可以遍历它。
甚至可以定义一个"无限"迭代器,它永远不会"结束"并且总会返回一个新值(比如随机数、递增值、唯一标识符,等等)。
javascript
var randoms = {
[Symbol.iterator]: function () {
return {
next: function () {
return { value: Math.random() };
},
};
},
};
var randoms;
_pool = [];
for (var n of randoms) {
randoms_pool.push(n);
// 防止无限运行!
if (randoms_pool.length === 100) break;
}
原型
[[prototype]]
JavaScript
中的对象有一个特殊的 [[Prototype]]
内置属性,其实就是对于其他对象的引用。几乎所有的对象在创建时 [[Prototype]]
属性都会被赋予一个非空的值。
如果无法在对象本身找到需要的属性,就会继续访问对象的 [[Prototype]]
链。
可以通过 Object.create
来创建一个新对象,并指定它的 [[Prototype]]
。
javascript
var anotherObject = { a: 2 }; // 创建一个关联到anotherObject 的对象
var myObject = Object.create(anotherObject);
myObject.a; // 2
object.prototype
所有普通的 [[Prototype]]
链最终都会指向内置的 Object.prototype
。
属性的设置和屏蔽
给一个对象设置属性并不仅仅是添加一个新属性或者修改已有的属性值。
javascript
myObject.foo = 'bar';
接下来具体分析一下这条语句做了什么:
-
如果
myObject
对象中包含名为foo
的普通数据访问属性,这条赋值语句只会修改已有的属性值。 -
如果
foo
不是直接存在于myObject
中,[[Prototype]]
链就会被遍历,类似[[Get]]
操作。如果原型链上找不到foo
,foo
就会被直接添加到myObject
上。 -
如果属性名
foo
既出现在myObject
中也出现在myObject
的[[Prototype]]
链上层,那么就会发生屏蔽。myObject
中包含的foo
属性会屏蔽原型链上层的所有foo
属性,因为myObject.foo
总是会选择原型链中最底层的foo
属性。 -
如果
foo
不直接存在于myObject
中而是存原型在于原型链上层时会出现的三种情况:- 如果在
[[Prototype]]
链上层存在名为foo
的普通数据访问属性,并且没有被标记为只读(writable:false)
,那就会直接在myObject
中添加一个名为foo
的新属性,它是屏蔽属性。 - 如果在
[[Prototype]]
链上层存在foo
,但是它被标记为只读(writable:false)
,那么无法修改已有属性或者在myObject
上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。 - 如果在
[[Prototype]]
链上层存在foo
并且它是一个setter
,那就一定会调用这个setter
。foo
不会被添加到(或者说屏蔽于)myObject
,也不会重新定义foo
这个setter
- 如果在
如果你希望在第二种和第三种情况下也屏蔽 foo
,那就不能使用 =
操作符来赋值,而是使用 Object.defineProperty
来向 myObject
添加 foo
。