要说js令很多初学者头痛的点,那么this肯定是其中之一,但如果不理解this,很多开发任务就会出错。本文先梳理了常见的几种this的情况,然后再揪其本质,最后讲解与this相关的一些方法,希望能对其他有困惑的小伙伴有帮助。
一、this的指向规则
1.全局环境下执行函数,其中的this指向window
var message = 'window';
function fn() {
// use 'strict';
var message = 'fn';
return this.message;
}
fn(); // window
如果该函数开启了严格模式(即函数第一句的use 'strict'
),那么函数里面的this则指向undefined。因为严格模式规定了this不能指向window。
2.构造函数里的this指向实例对象
function Fun() {
this.getThis = function() {
return this;
}
}
var fun = new Fun();
console.log(fun.getThis()===fun, fun.getThis()===window); // true false
3.对象的方法的this指向.前面的对象
var a = {
b: function() { return this; },
c: {
d: function() { return this; }
}
};
console.log(a.b()===a, a.c.d()===a.c); // true true
这里要注意的一点是,如果将函数赋值给一个变量,再通过变量调用,则此时已经相当于直接在全局环境执行函数,this指向的会是window。
var fun = a.b;
console.log(fun()===window, fun()===a); // true false
实际上也是将函数b的地址值给了fun,所以通过fun调用已找不到上一级的a。而通过a.b则是先找到a的地址再找到b的地址,此时执行函数a.b()仍保持this指向a。
4.回调函数的this指向window
[1, 2, 3].forEach(function(value) {
console.log(this); // window
});
setTimeout(function() {
console.log(this); // window
}, 0);
回调函数的this之所以指向window,实际上是因为回调函数会先放到任务队列里,等到主执行栈为空的时候才拿出来执行,而此时主执行栈的环境为全局环境(又回到第一点)。
5.dom监听事件里的this指向dom
// html code
<body>
<button>click me</button>
</body>
// js code
document.querySelector('body').onclick = function(e) {
console.log(this, e.target); // 点击按钮时,this是body而e.target是button
}
二、this的指向本质
小伙伴们在开发中大多遇到的this就是上面的几种情况,记住了基本都够用了。但还是简单讲讲this内在的本质。
实际上我们执行函数的时候都是在执行fn.call(调用者)
。call就是绑定函数内部的this为第一个参数,当第一个参数为空对象{}、null、undefined时内部this指向window(后面还会讲call,此处知道这个就可以了)。
- 执行对象函数,调用者就是函数前面的东西,所以
a.b.c()
相当于c.call(a.b)
。 - 全局环境下执行函数,
fn()
前面没有调用者,实际上相当于fn.call(undefined)
,而当第一个参数为undefined时this就变成了window。
而由于第一个参数是调用者只有在调用的时候才知道,所以this其实是动态确定的,即没法定义时就确定,只有调用时才确定。
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
console.log(object.getNameFunc()()); //The Window
对于上面的例子同样分析,object.getNameFunc()
返回的是一个函数,相当于a.b.c()
中的c,而object.getNameFunc()
前面没有对象,所以最后的调用相当于object.getNameFunc().call(undefined)
,所以自然指向window而不是object。若要指向object该如何操作呢?一是变成object.getNameFunc().call(object)
,二是固定内部的this(非常常用)。
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
console.log(object.getNameFunc()()); //My Object
三、可以改变this的三个方法
1.call方法
fn.call(obj, a, b)
是以第一个参数(若为空对象{}、null、undefined时默认传入window)的身份去调用fn,后面的a、b即函数的参数。类似平时看到的obj.fn(a, b)
。而函数内的this就是传入的第一个参数。他的作用主要有以下三个:
(1) 借用原型方法:
// 获得编辑器中被选中的指定类型的节点
function getSelectorNodes (editor, type) {
var selection = window.getSelection(),
selectNodes = Array.prototype.filter.call(editor.querySelectorAll(type), function(item) {
return selection.containsNode(item, true) && item.tagName===type;
});
return selectNodes;
};
上面的例子,节点对象本身没有filter方法,但通过call借用了数组原型上的filter来使用。另一个借用场景是实例中的原生方法被覆盖了,也可以用此法调用原型方法。
var obj = {};
obj.hasOwnProperty('toString') // false
// 覆盖掉继承的 hasOwnProperty 方法
obj.hasOwnProperty = function () {
return true;
};
obj.hasOwnProperty('toString') // true
Object.prototype.hasOwnProperty.call(obj, 'toString') // false
(2) 将类数组对象转为数组:
类数组对象,指的是属性名为数字,并且有length的对象,但却不是真正的数组,没有数组的push等方法。如字符串、Dom元素集、函数的arguments等。
var str = 'hello world';
console.log(str instanceof Array); // false
如果要将这些类数组对象转成真正的数组,从而拥有数组的方法,可以通过slice来实现转换,具体代码如下:
var str = 'hello world';
var arr = Array.prototype.slice.call(str);
console.log(arr instanceof Array); // true
通过上面的代码可以将字符串转成真正的数组,从而拥有数组的push、forEach等方法。这里可能有人要问了,如果只是要用数组的方法,直接通过第一点说的call借用不可以吗?如Array.prototype.forEach.call(str, function(){})
。答案是肯定可以,只是此法处理的效率会比真正的数组处理要慢,所以还是推荐先转成数组再调用forEach。
(3) 绑定回调函数中的this:
如上面我们讲到的Dom监听事件里面的this,若要让他其中的this指向我们想要的对象而不是Dom对象本身,可以通过call来实现。
var obj = {};
var fn = function() {
console.log(this);
};
document.querySelector('body').onclick = function(e) {
fn.call(obj); // 如果不绑定obj,则默认this为body节点对象
};
2.apply方法
fn.apply(obj, [a, b])
大致与call相似,不同的是apply函数的参数是以数组的形式传进去的。而利用这点特性我们经常用apply来处理数组,其用处大概有以下四点:
(1) 借用其他方法处理数组:
var arr = [5, 15, 8, 3];
Math.max.apply(null, arr); // 借用max寻找数组的最大元素
对于函数的多个参数储存在数组里的情况,以往我们可能需要一个个取出来,但借用此法可以直接将数组作为参数传入进去,得到想要的结果。
(2) 将数组的空元素变为undefined:
由于函数参数传空的时候,函数内部读到该参数为视为undefined。所以通过apply方法,利用Array构造函数将数组的空元素变成undefined。
Array.apply(null, ['a', ,'b']); // [ 'a', undefined, 'b' ]
空元素与undefined的差别在于,数组的forEach、for...in、Object.keys()方法会跳过空元素,但是不会跳过undefined。因此,遍历内部元素的时候,会得到不同的结果。
(3) 将类数组对象转为数组。
(4) 绑定回调函数中的this。
3.bind方法
fn.bind(obj, a, b)
与call的传参很类似,但效果却有很大差别,call和apply调用是立即执行,而bind调用则是每次返回一个新的函数,这个函数里面的this被改成obj,且函数的参数绑定了默认初始值a, b。他的主要用处就是改变函数的this。
如上面的绑定回调函数的this,最好的实现使用bind。因为apply和call都会立即执行,所以上面使用call需要在外面嵌套一层使得函数在调用时才执行,而bind直接返回的就是个函数不执行,具体执行时机依据回调函数的规则。将上面代码改写如下:
var obj = {};
var fn = function() {
console.log(this);
};
document.querySelector('body').onclick = fn.bind(obj);
四、总结
最后依然来一个总结:
- 代码
调用者.函数()
调用实际上执行的是函数.call(调用者)
,函数中的this就是传入的调用者,当为{}、null、undefined时调用者就是window。 - 改变函数中的this有三个相关方法,当需要返回新函数时用bind,当需要立即执行时用call和apply;call常用于借用原型方法,apply常用于处理数组。
网友评论