最近在复习JS基础,所以这里将《你不知道的JavaScript》关于this的相关知识整理分享,记录学习过程。
上篇文章讲了js中的作用域,这里首先要强调一点就是,this在任何时候都不指向函数的作用域,作用域"对象"无法通过JavaScript代码访问,他存在于JavaScript引擎内部,每个函数的this都是在被调用的时候绑定的,完全取决于函数的调用位置。
一、绑定规则
1. 默认绑定
首先要说的就是独立函数调用,这条规则可以看成是无法应用其他规则时的默认规则。
看下面代码:
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2
复制代码
可以看到this.a被解析成了全局变量a,这就是因为在这里应用了this的默认绑定,也就是说,在代码中foo()是直接使用不带任何修饰的函数引用调用的,因此值能使用默认绑定。
这里需要注意的是如果使用严格模式,就不能将全局对象用于默认绑定,因此this绑定会绑定到undefined。
2.隐式绑定
思考下面代码:
function foo() {
console.log(this.a);
}
var obj = {
a:2,
foo:foo
}
obj.foo(); //2
复制代码
这里需要注意无论是直接在obj中定义还是先定义再添加为引用属性,这个函数严格来说都不属于obj对象。
但是当foo()函数被调用时,它的前面确实加上了对obj的引用。**当函数引用有上下文对象时,隐式绑定规则会把函数中的this绑定到这个上下文对象上。**因为调用foo时this被绑定到obj,因此this.a和obj.a是一样的。
3.显式绑定
上一条说了隐式绑定,那么如果现在我们不想在函数内部包含函数引用,而想在某个对象上强制调用函数,那么该怎么做呢?
这里我们可以使用call和apply方法来做到显示绑定,这两个方法的原理就是:他们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this因为这样可以直接指定this的绑定对象,因此我们称之为显示绑定.
function foo() {
console.log(this.a);
}
var obj = {
a: 2
};
foo.call(obj); // 2
复制代码
一、硬绑定
硬绑定是显示绑定的一个变种,可以解决this丢失问题,思考下面代码:
function foo() {
console.log(this.a);
}
var obj = {
a : 2
};
var bar = function () {
foo.call(obj);
}
bar(); // 2
bar.call(window); // 2
复制代码
从上面代码可以看出,硬绑定的bar不可能再次修改它的this值,在这里我们可以创建一个简单的重复使用的硬绑定辅助函数bind:
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来实现这种硬绑定模式:
function foo(somthing) {
console.log(this.a, something);
return this.a + something;
}
var obj = {
a:2
}
var bar = foo.bind(obj);
var b = bar(); // 2,3
console.log(b); // 5
复制代码
二、软绑定
硬绑定这种方式,可以把this强制绑定到指定的对象(除了使用new,后面讲),防止了函数应用默认绑定的同时也大大降低了函数的灵活度。
如果可以给默认绑定指定一个全局对象或者undefined之外的值,就可以实现和硬绑定相同的效果,同时也保留了隐式绑定或显示绑定修改this的能力。
思考下面软绑定代码:
if(!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this;
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;
}
}
复制代码
原理其实就是,首先检查调用时的this,如果this绑定到全局对象或者undefined,那就把指定的默认对象obj绑定到this,否则不会修改this。请看下面代码:
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();
obj2.foo(); // name: obj2 <-----!
fooObj.call(obj3); // name: obj3 <-----!
复制代码
可以看到,软绑定版本的foo()可以手动将this绑定到obj2或者obj3上,但如果应用默认绑定,则会将this绑定到obj。
4.new绑定
js中实际上并不存在所谓的"构造函数",只有对于函数的"构造调用", 使用new来调用函数,或者说发生构造函数调用时,会自动执行下面步骤:
- 创建一个全新的对象
- 这个新对象会被执行[[Prototype]]连接
- 这个新对象会绑定到函数调用的this
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
请看下面代码:
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log(bar.a);
复制代码
使用new来调用foo()时,我们会构造一个新的对象并把它绑定到foo调用中的this上。
二、优先级
如果在某个调用位置应用了多条规则,如何确定哪条规则生效?
obj.foo.call(obj2); // this指向obj2 显式绑定比隐式绑定优先级高。
new obj.foo(); // thsi指向new新创建的对象 new绑定比隐式绑定优先级高。
复制代码
显式绑定和new绑定无法直接比较(会报错),默认绑定是不应用其他规则之后的兜底绑定所以优先级最低,最后的结果是:
显式绑定 > 隐式绑定 > 默认绑定 new绑定 > 隐式绑定 > 默认绑定
三、this绑定规则总结
- 对于直接调用的函数来说,不管函数被放在哪里,this都是window
- 对于被别人调用的函数来说,被谁"点"出来的,this就是谁
- 在构造函数中,类中(函数体中)出现的this.xxx=xxx中的this是当前类的一个实例
- call、apply时,this是第一个参数。bind要优于apply和call
- 箭头函数没有自己的this,需要看其外层是否有函数,如果有,外层函数的this就是内部箭头函数的this,如果没有,则this是window