第1章 关于this
this
是自动定义在所有函数的作用域中的关键字,用于引用合适的上下文对象。
☞ 为什么要使用 this
?
-
this
提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计得更加简洁并且易于复用。
☞ 对 this
的误解
-
this
不指向函数自身,也不指向函数的词法作用域。- 作用域“对象”无法通过JavaScript代码访问,存在于JavaScript引擎内部
☞ this
到底是什么
-
this
是在函数被调用时发生的绑定,和函数声明的位置没有关系,它的上下文(指向)取决于函数调用时的各种条件。 - 当一个函数被调用时,会创建一个活动记录(执行上下文)。
- 这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。
this
就是记录的其中一个属性,会在函数执行的过程中用到。
- 这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。
第2章 this全面解析
☞ 调用位置(调用方法)
调用栈
:为了到达当前执行位置所调用的所有函数,类似于函数调用链。
调用位置就是在当前执行的函数的前一个调用中。
function baz() {
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域
console.log('baz');
bar(); // <-- bar 的调用位置
}
function bar() {
// 当前调用栈是:baz -> bar
// 因此,当前调用位置在 baz 中
console.log('bar');
}
baz(); // <-- baz 的调用位置
☞ 绑定规则
① 默认绑定:独立函数调用,this
指向全局对象
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2
严格模式下,全局对象将无法使用默认绑定,
this
会绑定到undefined
② 隐式绑定:考虑调用位置是否有上下文对象
function foo() {
console.log(this.a);
}
/**
* 无论是直接在 obj 中定义还是先定义再添加引用属性,foo() 严格来说都不属于 obj 对象
*/
var obj2 = {
a: 42,
foo: foo // 当做 obj 的引用属性添加
};
var obj1 = {
a: 2,
obj2: obj2
}
/**
* 对象属性引用链中只有最顶层或者说最后一层会影响调用位置。
* 调用位置使用 obj2 的上下文来引用函数
*/
obj1.obj2.foo(); // 42
当函数有上下文对象时,
隐式绑定
规则会把函数调用中的this
绑定到这个上下文对象。
※ 隐式丢失:被 隐式绑定
的函数会丢失绑定对象,即会应用 默认绑定
。
[例1:]
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a是全局对象的属性
bar(); // 'oops, global'
[例2:传入回调函数 ]
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'
// 传入语言内置的函数
setTimeout(obj.foo, 2000); // 'oops, global'
JavaScript 环境中内置的 setTimeout() 函数实现和下面的伪代码类似:
function setTimeout(fn, delay) {
// 等待 delay 毫秒
fn(); // <-- 调用位置
}
调用回调函数的函数可能会修改 this。
在一些流行的 JavaScript 库中事件处理器常会把回调函数的
this
强制绑定到触发事件的 DOM 元素上。实际上,你无法控制回调函数的执行方式,因此就没有办法控制会影响绑定的调用位置。
③ 显式绑定:call()、apply()
1) 硬绑定 :显式绑定的一个变种,解决丢失绑定问题
function foo() {
console.log(this.a);
}
var obj = {
a: 2
};
var a = 3;
/**
* 显式绑定
* 仍然存在丢失绑定问题
*/
foo.call(obj); // 2
foo.call(null); // 3
/**
* 硬绑定:显式的强制绑定
* 解决丢失绑定问题
*/
var bar = function() {
foo.call(obj);
};
bar(); // 2
setTimeout(bar, 100); // 2
// 硬绑定的 bar 不可能再修改它的 this
bar.call(window); // 2
[ 硬绑定的典型应用场景 ]:创建一个包裹函数,传入所有的参数并返回接收到的所有值。
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
var obj = {
a: 2
};
var bar = function() {
return foo.apply(obj, arguments);
};
var b = bar(3); // 2 3
console.log(b); // 5
[ 硬绑定的应用场景2 ]:创建一个 i
可重复使用的辅助函数(bind实现及内置函数)
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
/**
* 简单的辅助绑定函数:
* 返回一个硬编码的新函数,把参数设置为 this 的上下文并调用原始函数
*/
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
/**
* 硬绑定模式内置方法:
* Function.prototype.bind
*/
var bar2 = foo.bind(obj);
var b2 = bar(4); // 2 4
console.log(b2); // 6
2) API调用的“上下文” :提供“上下文”的可选参数,确保回调函数使用指定的 this
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"
④ new
绑定
在 JavaScript 中,构造函数只是有些使用 new
操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
使用 new
来调用函数时,会自动执行下面的操作:
1)创建(或者说构造)一个全新的对象;
2)这个新对象会被执行[[原型]]连接;
3)这个新对象会绑定到函数调用的
this
;4)如果函数没有返回其他对象,那么
new
表达式中的函数调用会自动返回这个新对象。
☞ 优先级/判断 this
- 由
new
调用?——> 绑定到新创建的对象。 - 由
call
或apply
(或者bind
)调用?——> 绑定到指定的对象。 - 由上下文对象调用?——> 绑定到那个上下文对象。
- 默认——> 在严格模式下绑定到
undefined
,否则绑定到全局对象。
[例子]:
function foo(p1, p2) {
this.val = p1 + p2;
}
// 是所以使用 null 是因为在本例中我们并不关心硬绑定的 this 是什么
// 反正使用 new 时 this 会被修改
var bar = foo.bind(null, 'p1');
var baz = new bar('p2');
console.log(baz.val); // p1p2
在
new
中使用硬绑定函数,主要目的是预先设置函数的一些参数,这样在使用new
进行初始化时就可以只传入其余的参数。bind(...)的功能之一就是可以把第一个参数(第一个参数用于绑定 this)之外的其他参数都传给下层的函数(这种技术成为“部分应用”,是“柯里化”的一种)。
☞ 绑定例外
① 被忽略的 this
如果你把 null
或者 undefined
作为 this
的绑定对象传入 call
、apply
或者 bind
,这些值在调用时会被忽略,实际应用的是默认绑定规则。
-
传入
null
的情况:使用
apply(...)
来“展开”一个数组,并当做参数传入一个函数。-
bind(...)可以对参数进行柯里化(预先设置一些参数)。
function foo(a, b) {
console.log("a:" + a + ", b:" + b);
} // 把数组“展开”成参数
foo.apply(null, [2, 3]); // a:2, b:3 // 在ES6中,可以用...操作符代替apply(...)来“展开”数组
foo(...[1,2]); // a:1, b:2 // 使用 bind(...) 进行柯里化
var bar = foo.bind(null, 3);
bar(4); // a:3, b:4
-
更安全的
this
:-
创建一个空的非委托的对象(
Object.create(null)
)function foo(a, b) {
console.log("a:" + a + ", b:" + b);
} // 创建DMZ(demilitarized zone,非军事区)空对象
var dmzObj = Object.create(null); // 把数组“展开”成参数
foo.apply(dmzObj, [2, 3]); // a:2, b:3 // 使用 bind(...) 进行柯里化
var bar = foo.bind(dmzObj, 3);
bar(4); // a:3, b:4
-
Object.create(null)
和{}
很像,但是并不会创建Object.prototype
这个委托,所以它比 {} “更空”。
② 间接引用 —— 函数会应用默认绑定规则。
[ “间接引用”最容易在赋值时发生 ]:
function foo() {
console.log(this.a);
}
var a = 2;
var o = {a: 3, foo: foo};
var p = {a: 4};
o.foo(); // 3
/**
* 该赋值表达式的返回值是目标函数的引用
* 因此调用位置是 foo() 而不是 p.foo() 或者 o.foo()
*/
(p.foo = o.foo)(); // 2
③ 软绑定
硬绑定的优点:会把 this
强制绑定到指定的对象,防止函数调用应用默认绑定规则。
硬绑定的缺点:会大大降低函数的灵活性,使用之后就无法使用隐式绑定或者显示绑定来修改 this
。
软绑定:可以给默认绑定指定一个全局对象和 undefined
以外的值(同硬绑定),同时保留隐式绑定或者显式绑定修改 this
的能力。
if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this;
// 捕获所有 curried 参数
var curried = [].slice.call(arguments, 1);
var bound = function() {
return fn.apply(
(!this || this === (window || global)) ?
obj : this,
curried.concat.apply(curried, arguments)
);
};
bound.prototype = Object.create(fn.prototype);
return bound;
}
}
function foo() {
console.log("name:" + this.name);
}
var obj = { name: 'obj' },
obj2 = { name: 'obj2' },
obj3 = { name: 'obj3' };
/**
* 软绑定
*/
var fooOBJ = foo.softBind(obj);
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!!
fooOBJ.call(obj3); // name: obj3 <---- 看!
setTimeout(obj2.foo, 10); // name: obj <---- 应用了软绑定
/**
* 硬绑定
*/
obj3.foo = foo.bind(obj3);
obj3.foo(); // name: obj3
setTimeout(obj3.foo, 10); // name: obj3
☞ this
词法 ——> 箭头函数
箭头函数
不使用 this
的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this
。
[ 箭头函数的词法作用域 ]:
function foo() {
// 返回一个箭头函数
return (a) => {
// this 继承自 foo()
console.log(this.a);
};
}
var obj1 = {
a: 2
};
var obj2 = {
a: 3
};
var bar = foo.call(obj1);
bar.call(obj2); // 2,不是3!箭头函数的绑定无法被修改!
[ 箭头函数最常用于回调函数中 ]:
function foo() {
setTimeout(() => {
// 这里的 this 在此法上继承自 foo()
console.log(this.a);
}, 100);
}
var obj = {
a: 2
};
foo.call(obj); // 2
箭头函数可以像
bind(...)
一样确保函数的this
被绑定到指定对象,此外,其重要性还体现在它用更常见的词法作用域取代了传统的this
机制。