整理自《你不知道的JavaScript(上卷)》
this 是个什么东西?它是 运行时绑定的 一个记录属性,上下文取决于函数调用时的各种条件。看看书里的具体解释是什么:
当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。
为什么要使用 this?为了引用合适的上下文对象。 什么是“合适”而什么又是“上下文对象”我们将在下文中展开探讨。
既然是为了引用合适的上下文对象,我们得把这个 this 放在合适的地方绑定。接下来介绍四种常见的绑定规则。
就是最最一般的情况啦,看下面的例子。foo 函数是绑定在全局对象上的(也就是想象最顶层有一个全局对象,foo 是它身上的一个函数挂件),调用的时候 this 就会指向全局对象中的那个 a ,也就是外面的那个 2 。
在严格模式下会报错 undefined
function foo(){
console.log(this.a);
}
var a = 2;
foo(); // 2
隐式绑定也是一种非常常见的绑定形式,个人觉得是更通俗意义上的默认绑定方式。牢记一句话:
this 绑定在运行时最后一个调用它的对象上
如果隐式绑定丢失了自己的绑定对象,便会执行默认绑定,将函数绑定在 window 全局对象上。
看下面几个例子。
function foo(){
console.log(this.a);
}
var obj1 = {
var a = 42,
foo: foo
}
var obj2 = {
var a = 21;
obj1: obj1
}
obj2.obj1.foo() // 42
------------------------------------------
var bar = obj1.foo();
var a = "okok 我最ok"
bar() // "okok 我最ok"
------------------------------------------
function doFun(fn){
fn()
}
doFun(obj1.foo) // "okok 我最ok"
这个例子中第一个输出语句(第12行)最后调用 foo 函数的是 obj1 对象,因此 this 是指向 obj1 中的 a 这个变量的,输出42,没毛病。第二个输出语句(第15行)的函数 bar 是孤零零直接被调用的,那么就会直接绑定到最外面的那个 window 对象,挂在 window 对象上的 a 是啥呢?就是"okok 我最ok"。即使 bar 是由 obj1 赋值而来,那都不用看,不管前面有多少花里胡哨的赋值调用,只需要看最后运行时是谁调用它就行。
第三个输出语句有点怪异,上面不是说看运行时谁调用它吗?这下是 obj1 调用的,为什么输出的是全局对象上挂载的变量呢?
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值
这句话就是说,可以把 obj1.foo 当作一个整体(也就是函数 foo)赋值给了 doFun,那么在调用 doFun 的时候,实际上还是挂载在最外层的全局对象 window 上了,因此仍然输出了“ok”语句,并没有违反最终运行调用的规则。
显式绑定的意思是用一些方法在某个对象上强制调用函数。比如使用 call、apply 和 bind 显式绑定 this 的上下文对象。那么接下来就不得不整理一下这三者的区别了。
call | apply | bind |
---|---|---|
funcA.call(obj, arg1, arg2, …) | funcA.apply(obj, [args…]) | funcA.bind(obj) |
在调用 funcA 函数时,funcA 的this 指向传入的第一个参数,即 obj,后面的参数为调用 funcA 的其他参数【立即执行】 | 和 call 基本一致,只是传参的形式不同,apply 将其他参数合并为数组传入【立即执行】 | bind 只是作绑定,与前两个一样,funcA 执行的时候会将 obj 作为 this 的指向值,不同点在于它返回的是一个函数而不是执行结果【手动执行】 |
可惜,显式绑定仍然无法解决我们之前提出的丢失绑定问题。
这句话是什么意思呢?首先应该为所谓“丢失绑定”下一个定义,因为前面说了,this 会指向最后一个调用它的对象,那么这个 this 的指向将会是飘忽不定的。比如说在回调函数中,虽然一开始使用上述显式绑定的方法明确了 this 的指向,但在异步操作结束执行回调的时候,这个 this 可就不一定顺着我们的心意了。所以我们需要下面的办法:硬绑定。
搞几个例子看看。
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
---------------------------------------------------------------------
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
上述结果都正确地输出了我们的理想值,那它为什么叫“硬绑定”呢?因为在 bar 函数的内部,foo 的 this 被强制(显式)绑定到了 obj 上,后面的函数不论怎样调用它都能执行我们理想的结果。
而上面第二个例子中的 bind ,成为了ES5就提供的内置方法 Function.prototype.bind
。面试官再让你手写 bind 的时候,就应该信手拈来啦~
最后一种改变 this 指向的方法了,加油!
在 js 中,构造函数并不会实例化一个类或者创建一个类,当我们使用 new 操作符的时候,只是调用了一些普通的函数,这个机制与其他语言不一样。即书中所说:
不存在所谓“构造函数”,只有对函数的“构造调用”。
当我们使用 new 来调用函数时,会发生以下几件事:
举个例子:
function Mother(name){
this.name = name;
}
var son = new Mother("Da");
son.__proto__ = Mother.prototype;
Mother.call(son, "da");
son.name
son
废话不多说:显式绑定 > 隐式绑定 > 默认绑定,如果是 new 绑定,则会创建一个新的对象。
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;
};
}
可以给默认绑定指定一个全局对象和undefined以外的值,同时保留隐式绑定或者显式绑定修改this的能力
关键语句是 !this || this === (window || global) ? obj : this
,即当发现传入的 this 为空或者是全局对象 window 时,将 this 指向默认的对象,其他时候保留调用时的 this。
书中还提到了函数柯里化,这块内容可以参考 一些js的奇淫技巧 中关于函数柯里化的部分。其实个人觉得软绑定也属于一种技巧型的操作。
贴一下语雀原文链接,阅读体验更佳
欢迎交流!