【你不知道的JavaScript】this关键字

发布时间 2023-11-15 17:32:28作者: unuliha

没有this时,需要传入上下文获取name,在多个上下文时,代码变得繁杂重复

var me = {
    name: "Kyle"
};
var you = {
    name: "Reader"
};

function identify(context) {
    return context.name.toUpperCase();
}
function speak(context) {
    var greeting = "Hello, I'm " + identify (context);
    console.log(greeting);
}

identify(you); // READER
speak(me); //hello, I'm KYLE

有了this后,代码复用性增加,且更加简洁

function identify() {
    return this.name.toUpperCase();
}
function speak() {
    var greeting = "Hello, I'm " + identify.call(this);
    console.log(greeting);
}

this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。this绑定方式包括默认绑定、隐式绑定、显式绑定(硬绑定)、new操作符绑定。

// 例1 默认绑定
function foo() {
    console.log(this.a);
}
var a = 2;
foo(); // 2 ,foo在全局作用域调用,this指向全局对象,在全局对象上找a

// 例2 默认绑定
function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
obj.foo(); // 2 ,foo的上下文对象变为obj,其this指向obj

// 例3 隐式绑定
function foo() {
    console.log(this.a);
}
var obj2 = {
    a: 42,
    foo: foo
};
var obj1 = {
    a: 2,
    obj2: obj2
};
obj1.obj2.foo(); // 42 ,foo的上下文对象是obj2,其this指向obj2

// 例4 隐式绑定
function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a是全局对象的属性
bar(); // "oops, global" ,bar引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用,上下文对象是全局对象

// 例5 隐式绑定
function foo() {
    console.log(this.a);
}
function doFoo(fn) {
    // fn其实引用的是foo
    fn(); // <-- 调用位置!
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global"; // a是全局对象的属性
doFoo(obj.foo); // "oops, global",同理,this是在调用时确认,此时真正调用的位置this指向外部全局对象。

// 例6 隐式绑定
function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global"; // a是全局对象的属性
setTimeout(obj.foo, 100); // "oops, global" 内置函数setTimeout相当于包了一层函数调用

// setTimeout的实现类似下面的伪代码
function setTimeout(fn, delay) {
    // 等待delay毫秒
    fn(); // <-- 调用位置!
}

// 例7 通过call改变this指向,硬绑定
function foo() {
    console.log(this.a);
}
var obj = {
    a:2
};
foo.call(obj); // 2

// 例8 通过call绑定this后,不能再改变this的指向(apply同理),称之为硬绑定
function foo() {
    console.log(this.a);
}
var obj = {
    a:2
};
var bar = function() {
    foo.call(obj);
};
bar(); // 2
setTimeout(bar, 100); // 2
// 硬绑定的bar不可能再修改它的this,实际上是foo在调用时被分配了上下文(this),因此不再向外找this,外层的bar被指定任何上下文不对其产生影响
bar.call(window); // 2

// 例9 硬绑定典型应用场景——包裹函数
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,对于任何入参,都将其与对象obj的属性进行协同操作,并按设定的规则返回值
console.log(b); // 5

// 例10 例9中的obj也可以抽象为参数
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

// 例11 例10中的操作在JavaScript已经被封装为内置函数bind
function foo(something) {
    console.log(this.a, something);
    return this.a + something;
}
var obj = {
    a:2
};
var bar = foo.bind(obj); // 将foo函数中的this绑定为obj
var b = bar(3); // 2 3
console.log(b); // 5

// 例11 内置函数的上下文参数
function foo(el) {
    console.log(el, this.id);
}
var obj = {
    id: "awesome"
};
// 调用foo(..)时把this绑定到obj
[1, 2, 3].forEach(foo, obj);// forEach传入的第二个参数就是上下文
// 1 awesome 2 awesome 3 awesome

// 例12 new 绑定
function foo(a) {
    this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2,使用new来调用foo(..)时,我们会构造一个新对象并把它绑定到foo(..)调用中的this上

四种绑定方式存在优先级:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

// 例13 显式绑定优先级高于隐式绑定
function foo() {
    console.log(this.a);
}
var obj1 = {
    a: 2,
    foo: foo
};
var obj2 = {
    a: 3,
    foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call(obj2); // 3
obj2.foo.call(obj1); // 2

// 例14 new绑定优先级高于隐式绑定
function foo(something) {
    this.a = something;
}
var obj1 = {
    foo: foo
};
var obj2 = {};

obj1.foo(2);
console.log(obj1.a); // 2

obj1.foo.call(obj2, 3);
console.log(obj2.a); // 3

var bar = new obj1.foo(4);
console.log(obj1.a); // 2
console.log(bar.a); // 4

绑定例外场景

// 例15 call将this绑定到null或undefined时,会应用默认绑定
function foo() {
    console.log(this.a);
}
var a = 2;
foo.call(null); // 2

// 例16 将this绑定到null的应用——函数柯里化
function foo(a, b) {
    console.log("a:" + a + ", b:" + b);
}
// 把数组“展开”成参数
foo.apply(null, [2, 3]); // a:2, b:3
// 使用bind(..)进行柯里化
var bar = foo.bind(null, 2);
bar(3); // a:2, b:3

// 例17 使用null来忽略this绑定会产生难以想象的结果,更安全的做法
function foo(a, b) {
    console.log("a:" + a + ", b:" + b);
}
// 我们的DMZ空对象,“DMZ”(demilitarized zone,非军事区)对象——它就是一个空的非委托的对象
var ø = Object.create(null);
// 把数组展开成参数
foo.apply(ø, [2, 3]); // a:2, b:3
// 使用bind(..)进行柯里化
var bar = foo.bind(ø, 2);
bar(3); // a:2, b:3

// 例18 间接引用
function foo() {
    console.log(this.a);
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };

o.foo(); // 3
(p.foo = o.foo)(); // 2,赋值表达式p.foo = o.foo的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo()。

// 例19 软绑定,解决使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改this的问题
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;
    };
}

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(obj);
obj2.foo(); // name: obj2 <---- 看!! !

fooOBJ.call(obj3); // name: obj3 <---- 看!

setTimeout(obj2.foo, 10);
// name: obj   <---- 应用了软绑定

箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。

// 例20 箭头函数的词法作用域
function foo() {
    // 返回一个箭头函数
    return (a) => {
      //this继承自foo()
      console.log(this.a);
    };
}
var obj1 = {
    a:2
};
var obj2 = {
    a:3
};
var bar = foo.call(obj1);
bar.call(obj2); // 2, 不是3!
// foo()内部创建的箭头函数会捕获调用时foo()的this。由于foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改。

// 例21 对比例20
function foo() {
    // 返回一个函数
    return function(a){
      console.log(this.a);
    };
}
var obj1 = {
    a:2
};
var obj2 = {
    a:3
};
var bar = foo.call(obj1);
bar.call(obj2); // 3,上下文被改变
foo.call(obj1).call(obj2); // 3,

// 例22 回调函数中的this
function foo() {
    setTimeout(() => {
      // 这里的this在词法上继承自foo()
      console.log(this.a);
    },100);
}
var obj = {
    a:2
};
foo.call(obj); // 2

补充说明:例8中的硬绑定第二个及之后的call不能改变this指向,为什么例21后面的call能改变this指向?

function foo() {
    console.log(this.a);
    console.log(this); // obj
}
var obj = {
    a:2
};
var bar = function() {
    foo.call(obj); // foo在此处被调用,指定了上下文为obj,即this指向obj
    console.log(this); // window
};
bar(); // bar在全局上被调用,上下文为全局,即this指向window

// 再看例8,bar在全局上下文被调用,bar的this(上下文)就是window;foo的this是obj,foo有自己的this,不再向外找this
function foo() {
    console.log(this.a);
}
var obj = {
    a:2
};
var bar = function() {
    foo.call(obj);
};
bar(); // 2

// 变体,此时foo调用处没找到this,向外找,最后找到window,因此取的是window上a
function foo() {
    console.log(this.a);
}
var obj = {
    a:2
};
var bar = function() {
    foo();
};
a = 11;
bar(); // 11;

// 再看例21,bar在声明时调用了foo,给foo指定了obj1为其上下文,在调用bar时,又给bar指定了obj2为bar的上下文(this),因此bar的this就是obj2,而foo的上下文不再对其产生影响。在例20中箭头函数没有自己的this,只能向外找,最后找到foo中的this作为其上下文。
function foo() {
    // 返回一个函数
    return function(a){
      console.log(this.a);
    };
}
var obj1 = {
    a:2
};
var obj2 = {
    a:3
};
var bar = foo.call(obj1); 
bar.call(obj2); // 3,上下文被改变
foo.call(obj1).call(obj2); // 3