# this

如果对于有经验的 JavaScript 开发者来说 this 都是一种非常复杂的机制,那它到底有用在哪里呢?真的值得付出这么大的代价学习吗?

this的指向,是在调用函数时根据执行上下文所动态确定的。

  1. 在函数体中,简单调用该函数时(非显式/隐式绑定下),严格模式下 this 绑定到 undefined,否则绑定到全局对象 window/global;
  2. 一般构造函数 new 调用,绑定到新创建的对象上;
  3. 一般由 call/apply/bind 方法显式调用,绑定到指定参数的对象上;
  4. 一般由上下文对象调用,绑定在该对象上;
  5. 箭头函数中,根据外层上下文绑定的 this 决定 this 指向。
function identify() {
  return this.name.toUpperCase();
}
function speak() {
  var greeting = "Hello, I'm " + identify.call( this );
  console.log( greeting );
}
var me = {
  name: "Kyle"
};
var you = {
  name: "Reader"
};
identify.call( me ); // KYLE
speak.call( you ); // Hello, 我是 READER
//如果不使用 this ,那就需要给 identify() 和 speak() 显式传入一个上下文对象。
function identify(context) {
  return context.name.toUpperCase();
}
function speak(context) {
  var greeting = "Hello, I'm " + identify( context );
  console.log( greeting );
}
var me = {
  name: "Kyle"
};
var you = {
  name: "Reader"
};

identify( you ); // READER
speak( me ); //hello, 我是 KYLE

this 提供一种更优雅的方式来隐式“传递”一个对象引用,因此可将 API 设计得更 简洁且易复用 。随着模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用 this则不会这样。

提示

this 既不指向函数自身也不指向函数的词法作用域。this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

# this调用位置

调用位置就是函数在代码中被调用的位置(而不是声明的位置)。

 function foo(num) {
    console.log("foo: " + num);
    // 记录 foo 被调用的次数
    console.warn(this.count) // 第一次undefined 后续就是NaN
    this.count++;
  }
  foo.count = 0;
  var i;
  for (i = 0; i < 10; i++) {
    if (i > 5) {
      foo(i);
    }
  }
  // foo: 6
  // foo: 7
  // foo: 8
  // foo: 9
  // foo 被调用了多少次?
  console.log(foo.count); // 0 -- WTF?
  console.log(window.count)// NaN

console.log 语句产生了 4 条输出,证明 foo(..) 确实被调用了 4 次,但是 foo.count 仍然是 0。显然从字面意思来理解 this 是错误的。

执行 foo.count = 0 时,的确向函数对象 foo 添加了一个属性 count。但是函数内部代码this.count 中的 this 并不是指向那个函数对象,所以虽然属性名相同,根对象却并不相同,困惑随之产生。

实际上,这段代码在无意中创建了一个全局变量 count,它的值最终有undefined转为NaN。

# 函数指向自身

如果要从函数对象内部引用它自身,那只使用 this 是不够的。一般来说你需要通过一个指向函数对象的词法标识符(变量)来引用它。

  function foo() {
    foo.count = 4; // foo 指向它自身
    console.log(foo)
  }
  foo()

  // ƒ foo() {
  //   foo.count = 4; // foo 指向它自身
  //   console.log(foo)
  // }
setTimeout( function(){
// 匿名(没有名字的)函数无法指向自身
}, 10 );

第一个函数被称为具名函数,在它内部可以使用 foo 来引用自身。

但是在第二个例子中,传入 setTimeout(..) 的回调函数没有名称标识符(这种函数被称为匿名函数),因此无法从函数内部引用自身。

# arguments.callee

还有一种传统的但是现在已经被弃用和批判的用法,是使用 arguments.callee 来引用当前正在运行的函数对象。这是唯一一种可以从匿名函数对象内部引用自身的方法。然而,更好的方式是避免使用匿名函数,至少在需要自引用时使用具名函数(表达式)。arguments.callee 已经被弃用,不应该再使用它。

尽管 arguments.callee 提供了一种灵活的方式来引用当前执行的函数,但它也有一些性能上的缺点。具体来说,每次递归调用时都需要重新创建 arguments 对象,这会影响现代浏览器的性能,特别是在递归深度较大的情况下。此外,arguments.callee 还会影响闭包的优化。

需要明确的是,this 在任何情况下都不指向函数的词法作用域。

# 绑定规则

# 默认绑定

this默认绑定可以理解为函数调用时无任何调用前缀的情景,默认绑定时this指向全局对象(非严格模式)

function fn1() {
    let fn2 = function () {
        console.log(this); //window
        fn3();
    };
    console.log(this); //window
    fn2();
};

function fn3() {
    console.log(this); //window
};

fn1();

这个例子中无论函数声明在哪,在哪调用,由于函数调用时前面并未指定任何对象,这种情况下this指向全局对象window。

但需要注意的是,在严格模式环境中,默认绑定的this指向undefined,来看个对比例子:

function fn() {
	console.log(this); //window
	console.log(this.name);//听风是风
};

function fn1() {
	"use strict";
	console.log(this); //undefined
	console.log(this.name);//Uncaught TypeError: Cannot read property 'name' of undefined
};

var name = '听风是风';

fn(); 
fn1() 
"use strict";
var name = '听风是风';
function fn() {
    console.log(this); //undefined
    console.log(this.name);//报错
};
fn();

如果在严格模式下调用不在严格模式中的函数,并不会影响this指向

var name = '听风是风';
function fn() {
    console.log(this); //window
    console.log(this.name); //听风是风
};

(function () {
    "use strict";
    fn();
}());

# 隐式绑定

如果函数调用时,前面存在调用它的对象,那么this就会隐式绑定到这个对象上,即:隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象

如果函数调用前存在多个对象,this指向距离调用自己最近的对象

//对象属性引用链中只有最顶层或者说最后一层会影响调用位置。举例来说:
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
function fn() {
    console.log(this.name);
};
let obj = {
    func: fn,
};
let obj1 = {
    name: '听风是风',
    o: obj
};
obj1.o.func() //undefined

这里输出undefined,千万不要将作用域链和原型链弄混淆了,obj对象虽然obj1的属性,但它两原型链并不相同,并不是父子关系,由于obj未提供name属性,所以是undefined。

打印o2.fn()修改结果为o2的操作。

const o1 = {
  text: 'o1',
  fn: function () {
    return this.text;
  },
};

const o2 = {
  text: 'o2',
  fn: o1.fn,
};

console.log(o2.fn()); // o2

this 指向最后调用它的对象,在 fn 执行时,挂到 o2 对象上即可,我们提前进行了赋值操作。

# 隐式丢失

隐式丢失最常见的就是作为参数传递以及变量赋值/函数别名 ,一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式

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 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

一种更微妙、更常见并且更出乎意料的情况发生在传入回调函数时:

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"

参数传递其实就是隐式赋值。相当于var fn=obj.foo,与创建别名的结果一样,应用了默认绑定,应该注意的是,return返回一个函数时,也是应用了默认绑定,因此传入函数时也会被隐式赋值。

隐式绑定丢失并不是都会指向全局对象:虽然丢失了 obj 的隐式绑定,但是在赋值的过程中,又建立了新的隐式绑定,这里this就指向了对象 obj1

var name = '行星飞行';
let obj = {
    name: '听风是风',
    fn: function () {
        console.log(this.name);
    }
};
let obj1 = {
    name: '时间跳跃'
}
obj1.fn = obj.fn;
obj1.fn(); //时间跳跃

# 显示绑定

在js中,当调用一个函数时,习惯称之为函数调用,函数处于一个被动的状态;而call与apply让函数从被动变主动,函数能主动选择自己的上下文,所以这种写法我们又称之为函数应用。

注意,如果在使用call之类的方法改变this指向时,指向参数提供的是null或者undefined,那么 this 将指向全局对象。另外,在js API中部分方法也内置了显式绑定,以forEach为例

let obj = {
    name: '听风是风'
};

[1, 2, 3].forEach(function () {
    console.log(this.name);//听风是风*3
}, obj);
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

然而,总是使用 null 来忽略 this 绑定可能产生一些副作用。如果某个函数确实使用了this(比如第三方库中的一个函数),那默认绑定规则会把 this 绑定到全局对象(在浏览器中这个对象是 window),这将导致不可预计的后果(比如修改全局对象)。

# DMZ规避

可以创建一个“DMZ”(demilitarizedzone,非军事区)对象——它就是一个空的非委托的对象

function someFunction() {
  // 假设这个函数是第三方库提供的,并且它使用了 this
  console.log(this); // 在非严格模式下,如果 this 被显式地设置为 null 或 undefined,这里会打印出 window 对象
  this.someProperty = "I might be modifying the global object!";
}

// 在非严格模式下调用函数,并显式地将 this 设置为 null
var ø = Object.create( null );

someFunction.apply(ø);
// 检查 window 对象是否被修改
console.log(window.someProperty);  
console.warn(ø.someProperty)

call(..) 和 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
bar.call( window ); // 2

创建了函数 bar() ,并在它的内部手动调用了 foo.call(obj) ,因此强制把 foo 的 this 绑定到了 obj 。无论之后如何调用函数 bar ,它总会手动在 obj 上调用 foo 。这种绑定是一种显式的强制绑定,因此称之为 硬绑定

硬绑定的典型应用场景就是创建一个包裹函数,传入所有的参数并返回接收到的所有值:

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
console.log( b ); // 5

另一种使用方法是创建一个可以重复使用的辅助函数:

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

第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一 个可选的参数,通常被称为“上下文”(context),其作用和 bind(..) 一样,确保你的回调 函数使用指定的 this 。

function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
};
// 调用 foo(..) 时把 this 绑定到 obj
[1, 2, 3].forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome

这些函数实际上就是通过 call(..) 或者 apply(..) 实现了显式绑定,这样你可以少些一些代码。

# new绑定

准确来说,js中的构造函数只是使用new 调用的普通函数,它并不是一个类,最终返回的对象也不是一个实例,只是为了便于理解习惯这么说罢了。

那么new一个函数究竟发生了什么呢,大致分为三步:

  1. 以构造器的prototype属性为原型,创建新对象;

  2. 将this(可以理解为上句创建的新对象)和调用参数传给构造器,执行;

  3. 如果构造器没有手动返回对象,则返回第一步创建的对象

function Fn(){
    this.name = '听风是风';
};
let echo = new Fn();
console.log(echo.name)//听风是风

在上方代码中,构造调用创建了一个新对象echo,而在函数体内,this将指向新对象echo上(可以抽象理解为新对象就是this)。

# this绑定优先级

this绑定优先级为:

  • 显式绑定 > 隐式绑定 > 默认绑定
  • new绑定 >显式绑定(bind可以,call和apply则不可以比较) >隐式绑定 > 默认绑定
......
let echo = new Fn().call(obj);//报错 call is not a function
//显式>隐式
let obj = {
    name:'行星飞行',
    fn:function () {
        console.log(this.name);
    }
};
obj1 = {
    name:'时间跳跃'
};
obj.fn.call(obj1);// 时间跳跃
//new>隐式
obj = {
    name: '时间跳跃',
    fn: function () {
        this.name = '听风是风';
    }
};
let echo = new obj.fn();
console.log(echo.name)//听风是风

作用域链与原型链的区别:

当访问一个变量时,解释器会先在当前作用域查找标识符,如果没有找到就去父作用域找,作用域链顶端是全局对象window,如果window都没有这个变量则报错。

当在对象上访问某属性时,首选i会查找当前对象,如果没有就顺着原型链往上找,原型链顶端是null,如果全程都没找到则返一个undefined,而不是报错。

function foo(a) {
  this.a = a;
}

const obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); // 2

var baz = new bar(3)
console.log(baz.a) // 3

console.log(obj1)//{a: 2}

bar 函数本身是通过 bind 方法构造的函数,其内部已经对将 this 绑定为 obj1,它再作为构造函数,通过 new 调用时,返回的实例已经与 obj1 解绑。 也就是说:new 绑定修改了 bind 绑定中的 this,因此 new 绑定的优先级比显式 bind 绑定更高。

function foo() {
  return () => {
    console.log(this.a);
  };
}

const obj1 = {
  a: 2,
};

const obj2 = {
  a: 3,
};

const bar = foo.call(obj1);
bar.call(obj2); // 2

由于 foo() 的 this 绑定到 obj1,bar(引用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。

var a = 123;
const foo = () => (a) => {
console.log(this.a);
};

const obj1 = {
a: 2,
};

const obj2 = {
a: 3,
};

const bar = foo.call(obj1);
bar.call(obj2)
// 123
const a = 123;
const foo = () => (a) => {
  console.log(this.a);
};

const obj1 = {
  a: 2,
};

const obj2 = {
  a: 3,
};

const bar = foo.call(obj1);
bar.call(obj2); // undefined

原因是因为使用 const/let 声明的变量不会挂载到 window 全局对象当中。因此 this 指向 window 时,自然也找不到 a 变量了。

参考1 (opens new window) 参考2 (opens new window)

最后更新: 11/10/2024, 7:57:43 PM