this和call、apply 、bind

2018-08-13  本文已影响0人  16manman

参考《JavaScript设计模式与开发实践》

this

跟别的语言大相径庭的是,JavaScript的this总是指向一个对象,而具体指向哪个对象是在运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境。

1. 作为对象的方法调用

当函数作为对象的方法被调用时,this指向该对象:

var obj = { 
    a: 1, 
    getA: function(){ 
        alert ( this === obj ); // 输出:true 
        alert ( this.a ); // 输出: 1 
    } 
}; 
obj.getA();
2. 作为普通函数调用。

当函数不作为对象的属性被调用时,也就是我们常说的普通函数方式,此时的this总是指向全局对象。在浏览器的JavaScript里,这个全局对象是window对象。

window.name = 'globalName';
var getName = function(){
    return this.name;
}
console.log(getName()); //输出:globalName 

//或者

var myObject = {
    name: 'sven',
    getName: function(){
        console.log('this',this);
        return this.name;
    }
}
var getName = myObject.getName;
console.log(getName());

有时候我们会遇到一些困扰,比如在div节点的事件函数内部,有一个局部的callback方法,callback被作为普通函数调用时,callback内部的this指向了window,但我们往往是想让它指向该div节点,见如下代码:

<html>
<body>
    <div id="div1">我是一个div</div>
</body>
<script> 
    window.id = 'window';
    document.getElementById('div1').onclick = function () {
        alert(this.id); // 输出:'div1' 
        var callback = function () {
            alert(this.id); // 输出:'window' 
        }
        callback();
    }; 
</script>
</html>

此时有一种简单的解决方案,可以用一个变量保存div节点的引用:

<html>
<body>
    <div id="div1">我是一个div</div>
</body>
<script> 
    window.id = 'window';
    document.getElementById('div1').onclick = function () {
        var that = this;
        var callback = function () {
            alert(that.id); // 输出:'div1' 
        }
        callback();
    }; 
</script>
</html>

在ECMAScript 5的strict模式下,这种情况下的this已经被规定为不会指向全局对象,而是undefined:

function func(){
    "use strict"
    alert(this); // 输出:undefined 
}
func(); 
3. 构造器调用

JavaScript中没有类,但是可以从构造器中创建对象,同时也提供了new运算符,使得构造器看起来更像一个类。
除了宿主提供的一些内置函数,大部分JavaScript函数都可以当作构造器使用。构造器的外表跟普通函数一模一样,它们的区别在于被调用的方式。当用new运算符调用函数时,该函数总会返回一个对象,通常情况下,构造器里的this就指向返回的这个对象,见如下代码:
宿主提供的一些内置函数:
javascript是一门编程语言,运行的环境是虚拟机(chrome是v8,别的浏览器也有),这个虚拟机在标准内称作javascript的运行时,这个运行时本身就是javascript的宿主环境了,不过在浏览器端,也把浏览器称作它的宿主环境(虚拟机寄宿在浏览器内)。
宿主我个人理解就是宿主对象,在浏览器里就是指window对象,内置函数指的就是window原型上的一些函数,例如new Date(); new Array();

var MyClass = function () {
    this.name = 'sven';
};
var obj = new MyClass();
alert(obj.name); // 输出:sven 

但用new调用构造器时,还要注意一个问题,如果构造器显式地返回了一个object类型的对象,那么此次运算结果最终会返回这个对象,而不是我们之前期待的this:

var MyClass = function () {
    this.name = 'sven';
    return {
        // 显式地返回一个对象
        name: 'anne'
    }
};
var obj = new MyClass();
alert(obj.name); // 输出:anne 

如果构造器不显式地返回任何数据,或者是返回一个非对象类型的数据,就不会造成上述问题:

var MyClass = function () {
    this.name = 'sven';
    return 'anne';  // 返回string类型
};
var obj = new MyClass();
alert(obj.name); // 输出:sven

4. Function.prototype.call或Function.prototype.apply调用。

跟普通的函数调用相比,用Function.prototype.call或Function.prototype.apply可以动态地改变传入函数的this:

var obj1 = {
    name: 'sven',
    getName: function () {
        return this.name;
    }
};
var obj2 = {
    name: 'anne'
};
console.log(obj1.getName()); // 输出: sven 
console.log(obj1.getName.call(obj2)); // 输出:anne 

call和apply方法能很好地体现JavaScript的函数式语言特性,在JavaScript中,几乎每一次编写函数式语言风格的代码,都离不开call和apply。在JavaScript诸多版本的设计模式中,也用到了call和apply。在下一节会详细介绍它们。

丢失的this

这是一个经常遇到的问题,我们先看下面的代码:

var obj = {
    myName: 'sven',
    getName: function () {
        return this.myName;
    }
};
console.log(obj.getName()); // 输出:'sven' 
var getName2 = obj.getName;
console.log(getName2()); // 输出:undefined 

当调用obj.getName时,getName方法是作为obj对象的属性被调用的,根据2.1.1节提到的规律,此时的this指向obj对象,所以obj.getName()输出'sven'。
当用另外一个变量getName2来引用obj.getName,并且调用getName2时,根据2.1.2节提到的规律,此时是普通函数调用方式,this是指向全局window的,所以程序的执行结果是undefined。
再看另一个例子,document.getElementById这个方法名实在有点过长,我们大概尝试过用一个短的函数来代替它,如同prototype.js等一些框架所做过的事情:

var getId = function (id) {
    return document.getElementById(id);
};
getId('div1'); 

我们也许思考过为什么不能用下面这种更简单的方式:

<html>
<body>
    <div id="div1">我是一个div</div>
</body>
<script> 
    var getId = document.getElementById;
    getId('div1'); 
</script>
</html>

在Chrome、Firefox、IE10中执行过后就会发现,这段代码抛出了一个异常。这是因为许多引擎的document.getElementById方法的内部实现中需要用到this。这个this本来被期望指向document,当getElementById方法作为document对象的属性被调用时,方法内部的this确实是指向document的。
但当用getId来引用document.getElementById之后,再调用getId,此时就成了普通函数调用,函数内部的this指向了window,而不是原来的document。
我们可以尝试利用apply把document当作this传入getId函数,帮助“修正”this:

document.getElementById = (function (func) {
    return function () {
        return func.apply(document, arguments);
    }
})(document.getElementById);
var getId = document.getElementById;
var div = getId('div1');
alert(div.id); // 输出:div1

个人尝试以下写法,也可行:

<html>
<body>
    <div id="div1">我是一个div</div>
</body>
<script>
    var getId = document.getElementById;
    var div = getId.call(document, 'div1');
    alert(div.id); // 输出:div1
</script>
</html>

call和apply

var func = function (a, b, c) {
    alert([a, b, c]); // 输出[ 1, 2, 3 ] 
};
func.apply(null, [1, 2, 3]); 

在这段代码中,参数1、2、3 被放在数组中一起传入func函数,它们分别对应func参数列表中的a、b、c。
call传入的参数数量不固定,跟apply相同的是,第一个参数也是代表函数体内的this指向,从第二个参数开始往后,每个参数被依次传入函数:

var func = function (a, b, c) {
    alert([a, b, c]); // 输出[ 1, 2, 3 ] 
};
func.call(null, 1, 2, 3); 

当调用一个函数时,JavaScript的解释器并不会计较形参和实参在数量、类型以及顺序上的区别,JavaScript的参数在内部就是用一个数组来表示的。从这个意义上说,apply比call的使用率更高,我们不必关心具体有多少参数被传入函数,只要用apply一股脑地推过去就可以了。
call是包装在apply上面的一颗语法糖,如果我们明确地知道函数接受多少个参数,而且想一目了然地表达形参和实参的对应关系,那么也可以用call来传送参数。
当使用call或者apply的时候,如果我们传入的第一个参数为null,函数体内的this会指向默认的宿主对象,在浏览器中则是window:

var func = function (a, b, c) {
    alert(this === window); // 输出true
};
func.apply(null, [1, 2, 3]); 

但如果是在严格模式下,函数体内的this还是为null:

var func = function (a, b, c) {
    "use strict"; 
    alert(this === null); // 输出true
};
func.apply(null, [1, 2, 3]); 

有时候我们使用call或者apply的目的不在于指定this指向,而是另有用途,比如借用其他对象的方法。那么我们可以传入null来代替某个具体的对象:

Math.max.apply( null, [ 1, 2, 5, 3, 4 ] ) // 输出:5 

bind例子:

const func1 = function(){
  console.log('this',JSON.stringify(this))
}
const obj = {
  name: 'obj'
}
const func2 = func1.bind(obj);
const func3 = func1;

func1();
func2();
console.log('func2 === func1',func2 === func1)
console.log('func3 === func1',func3 === func1)
上一篇下一篇

猜你喜欢

热点阅读