this关键字

原文地址:https://wangdoc.com/javascript/

涵义

前一张提到this可以用在构造函数之中,表示实例对象。除此之外,this还可以用在别的场合。但是不管是什么场合,this都有一个共同点:它总是返回一个对象。
简单的说,this就是属性或方法当前所在的对象。由于对象的属性可以赋给另一个对象。所以属性所在的当前对象是可变的,即this的指向是可变的。

var A = {
    name: "张三",
    describe: function () {
        return "姓名:" + this.name;
    }
};

var B = {
    name = "李四"
};

B.describe = A.describe;
B.describe() // "姓名:李四"

只要函数被赋值给另一个变量,this的指向就会变。

var A = {
    name: "张三",
    describe: function () {
        return "姓名:" + this.name;
    }
};

var name = "李四";
var f = A.describe;
f() // "姓名:李四"

实质

JavaScript语言之所以有this的涉及,跟内存里面的数据结构有关系。

var obj = {foo: 5};

上面的代码将一个对象赋值给变量obj。JavaScript引擎现在内存里面生成一个对象{foo:5},然后把这个对象的内存地址赋值给变量obj。也就是说,变量obj是一个地址(reference)。
原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。

{
    foo: {
        [[value]]: 5,
        [[writable]]: true,
        [[enumerable]]: true,
        [[configurable]]: true
    }
}

注意,foo属性的值保存在属性描述对象的value属性里面。
这样的结构是很清晰的,问题在于属性的值可能是一个函数。

var obj = { foo: function () {} };

这时,引擎会将函数单独保存在内存中,然后再将函数的地址赋给foo属性的value属性。由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。
JavaScript允许在函数体内部,引用当前环境的其他变量。所以需要一种机制,能够在函数体内部获取当前运行环境。所以this就出现了。

使用场合

this主要有以下几个使用场合。
(1)全局环境
全局环境使用this,它指的就是顶层对象window

this === window // true

function f() {
    console.log(this === window);
}
f() // true

(2)构造函数
构造函数中的this,指的是实例对象。
(3)对象的方法
如果对象的方法里面包含thisthis指向的就是方法运行时所在的对象。该方法赋值给另一个对象,就会改变this的指向。

var obj = {
    foo: function () {
        console.log(this);
    }
};

obj.foo() // obj

// 注意
(obj.foo = obj.foo)() // window
(false || obj.foo)() // window
(1, obj.foo)() // window

如果this所在的方法不在对象的第一层,这时this只是指向当前一层的对象,而不会继承跟上面的层。

var a = {
    p: "hello",
    b: {
        m: function () {
            console.log(this.p);
        }
    }
};

a.b.m() // undefined

如果这是将嵌套对象内部的方法赋值给一个变量,this依然会指向全局对象。

var hello = a.b.m;
hello() // 在window下运行
// 如果需要在引用b对象
var hello = a.b;
hello.m();

使用注意点

避免多层this

由于this的指向是不确定的,所以切勿在函数中包含多层的this

var o = {
    f1: function () {
        console.log(this);
        var f2 = function () {
            console.log(this);
        } ();
    }
};

o.f1()
// object
// window

解决方案

var o = {
    f1: function () {
        console.log(this);
        var that = this;
        var f2 = function () {
            console.log(that);
        } ();
    }
};
o.f1()
// object
// object

避免数组处理方法中的this

数组的mapforeach方法,允许提供一个函数作为参数。这个函数内部应该使用this

var o = {
    v: "hello",
    p: ["a1", "a2"],
    f: function () {
        this.p.forEach(function (item) {
            console.log(this.v + " " + item);
        });
    }
};

o.f()
// undefined a1
// undefined a2

上面代码中,forEach方法回调函数中的this,其实指向window对象,因此取不到o.v的值。解决这个问题的一种方法,使用中间变量固定this;另一个方法是将this当做forEach方法的第二个参数,固定它的运行环境。

避免回调函数中的this

回调函数中的this往往改变指向,最好避免使用。

绑定this的方法

this的动态切换,固然伟JavaScript创造了巨大的灵活性,但也使得编程变得困难和模糊。有时需要把this固定下来,避免出现意想不到的情况。JavaScript提供了callapplybind这三个方法来切换/固定this的指向。

Function.prototype.call()

函数实例的call方法,可以指定函数内部this的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。

var obj = {};
var f = function () {
    return this;
};
f() === window // true
f.call(obj) === obj // true

call方法的参数应该是一个对象。如果参数为空、nullundefined,则默认传入全局对象。
如果call方法的参数是一个原始值,那么这个原始值会自动转成对应的包装对象,然后传入call方法。

var f = function () {
    return this;
};

f.call(5) // Number {[[PromitiveValue]]: 5}

call方法还可以接受多个参数。
call的第一个参数就是this所要指向的那个对象,后面的参数则是函数调用时所需的参数。

function add(a, b) {
    return a + b;
}
add.call(this, 1, 2) // 3

call方法的一个应用是调用对象的原生方法。

var obj = {};
obj.hasOwnProperty("toString"); // false
// 覆盖掉继承的 hasOwnProperty方法
obj.hasOwnProperty = function () {
    return true;
};
obj.hasOwnProperty("toString") // true
Object.prototype.hasOwnProperty.call(obj, "toString") // false

Function.prototype.apply()

apply方法的作用与call方法类似,也是改变this指向,然后再调用该函数。唯一的区别就是,它是接受一个数组作为函数执行时的参数。
apply第一个参数也是this所要指向的对象,如果设为nullundefined,则等同于指定全局对象。

function f(x, y) {
    console.log(x + y);
}
f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2

利用这一点可以做一些有趣的应用:
(1)找出数组最大元素

var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a) // 10

(2)将数组的空元素变为undefined
通过apply方法,利用Array构造函数。

Array.apply(null, ["a",  , "b"])
// ["a", undefined, "b"]

空元素与undefined的区别在于,数组的forEach方法会跳过空元素。
(3)转换类似数组对象
利用数组对象的slice方法,可以将一个类似数组的对象转为真正的数组。

Array.prototype.slice.apply({0: 1, length: 1}) // [1]
Array.prototype.slice.apply({0: 1}) // []
Array.prototype.slice.apply({0: 1, length: 2}) // [1, undefined]
Array.prototype.slice.apply({length: 1}) // [undefined]

(4)绑定回调函数

var o = new Object();
o.f = function () {
    console.log(this === o);
};

var f = function () {
    o.f.apply(o);
};

// JQuery 写法
$("#button").on("click", f);

Function.prototype.bind()

bind方法用于将函数体内的this绑定到某个对象,然后返回一个新的函数。

var d = new Date()
d.getTime() // 

var print = d.getTime;
print() // Uncaught TypeError: this is not a Date object.

上面代码中,我们将d.getTime方法赋值给变量print,然后调用print就报错了。因为getTime方法内部的this,绑定Date对象的实例,赋给变量print以后,内部的this已经不指向Date对象的实例了。
bind方法可以解决这个问题。

var print = d.getTime.bind(d);
print();

bind方法的参数就是所要绑定的this对象。

var counter = {
    count: 0,
    inc: function () {
        this.count++;
    }
};
var func = counter.inc.bind(counter);
func();
counter.count // 1

因为成员方法中使用了this,指向counter对象的实例。所以需要使用bind来绑定thiscounter
bind还可以接受更多参数,将这些参数绑定原函数的参数。

var add = function (x, y) {
    return x * this.m + y * this.n;
};

var obj = {
    m: 2,
    n: 2
};

var newAdd = add.bind(obj, 5);
newAdd(5) // 20

上面代码中bind方法除了绑定this对象,还将add函数的第一个参数x绑定为5,然后返回一个新的函数newAdd,这个函数只需要在接受一个参数y就能运行了。
如果bind方法的第一个参数是nullundefined,等于将this绑定到全局对象,函数运行时this指向顶层对象。
bind方法有一些使用注意点。
(1)每一次返回一个新函数
bind方法每运行一次,就会返回一个新的函数,这会产生一些问题。比如,监听事件的时候不能写成下面这样。

element.addEventListener("click", o.m.bind(o));

上面代码,click事件绑定bind方法生成的一个匿名函数。这样会导致无法取消绑定。

element.removeEventListener("click", o.m.bind(o));

正确的写法是:

var listener = o.m.bind(o);
element.addEventListener("click", listener);
element.removeEventListener("click", listener);

(2)结合回调函数使用
回调函数是JavaScript最常用的模式之一,但是很常见的一个错误是,将包含this的方法直接当做回调函数。解决方法是使用bind方法。

var obj = {
    name: "hello",
    times: [1, 2, 3],
    print: function () {
        this.times.forEach(function (n) {
            console.log(this.name); 
        });
    }
};

obj.print() // 没有输出

上面代码中回调函数的this指向全局对象。解决这个问题也是通过bind方法绑定this

obj.print = function () {
    this.times.forEach(function (n) {
        console.log(this.name);
    }.bind(this));
};

obj.print() // hello

(3)结合call方法使用
利用bind方法,可以改写一些JavaScript原生方法的使用形式,以数组的slice方法为例。

[1, 2, 3].slice(0, 1) // [1]
// 相当于
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]

call方法实质上是调用Function.prototype.call方法,因此上面的表达式可以用bind方法改写。

var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]

如果再进一步,将Function.prototype.call方法绑定到Function.prototype.bind对象,就意味着bind的调用形式也可以被改写。

function f () {
    console.log(this.v);
}

var o = { v: 123 };
var bind = Function.prototype.call.bind(Function.prototype.bind);
bind(f, o)() // 123

第一个参数,函数实例,第二个参数this指向的对象。

原文地址:https://www.cnblogs.com/chris-jichen/p/10137893.html