📍this keyword and usage instructions

默认绑定

通常是纯粹或独立函数调用的情况下,这时 this 指向全局对象,在浏览器中为 window,而 Node.js 中为 global。当然,在纯粹函数调用时由于严格模式不允许默认绑定,this 为未定义的 undefined。可以把这条规则看作是无法应用其他规则时的默认规则。

// 单独使用;不论是否严格
console.log(this) // Window {0: Window, window: Window, self: Window, document: document, name: '', location: Location,…}
// 函数调用;非严格模式
global.x = "zs";
function zsall() {
	console.log(this.x);
}
zsall();  // zs
// 函数调用;严格模式
"use strict";
function usestrictzsall() {
	console.log(this);
 }
usestrictzsall() // undefined

隐式绑定

另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,在作为对象方法的调用场景时,this 指向方法的调用者。不过这种说法可能会造成一些误导。

// 经典题
global.name = "Kakashi";
function a () {
	var name = 'Sasuke';
	console.log(this.name);
}
function d (i) { // 形参i实际是函数
	return i() // 函数直接执行形参i
}
var b = {
	name: 'Naruto',
	detail: function() {
		console.log(this.name);
	},
	// 有括号需要执行
	// b.teacher => f(){return {function(){console.log(this.name)}}}
	// b.teacher() => f(){console.log(this.name)}
	teacher: function() {
		return function() {
			console.log(this.name);
		};
	},
}
var c = b.detail;
b.a = a; // 给b定义a属性
var e = b.teacher();
// global对象可以在全局作用域里通过使用 this 访问到
a(); // 纯粹的函数调用 global -- Kakashi
// ---
// b的第一条方法赋值给了变量c,那么真正的调用者是c
c(); // b的第一条方法看作普通函数赋值给c,已经和b没有关系了 -- Kakashi
// ---
b.a(); // Naruto
// ---
// 看着指向b,实际上是b.detail方法看成一个值用在d里面,实际执行的是全局范围的函数d,所以也是指向全局
d(b.detail) // Kakashi
e(); // 赋值了b.teacher()的输出=> var e = f(){console.log(this.name)} => 全局 -- Kakashi

一方面,对象属性引用链中只有最顶层或者说最后一层会影响调用位置。另一方面,最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式。

function foo() { console.log( this.a );}
var obj2 = {
    a: 42,
    foo: foo
};
var obj1 = {
    a: 2,
    obj2: obj2
};
obj1.obj2.foo(); // 42
// ex1 => 引用
// 虽然 bar 是 obj.foo 的一个引用,但是实际上它引用的是 foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定.
function foo() { console.log( this.a );}
var obj = {
    a: 2,
    foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"
// ex2 => 传入回调
// 参数传递其实就是一种隐式赋值,因此传入函数时也会被隐式赋值
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"
// ex3 => 把函数传入语言内置的函数
function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global"

除此之外,还有一种情况 this 的行为会出乎意料,即调用回调函数的函数可能会修改 this。在一些流行的 JavaScript 库中事件处理器常会把回调函数的 this 强制绑定到触发事件的 DOM 元素上。这在一些情况下可能很有用。

硬绑定

隐式绑定时,必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把 this 隐式绑定到这个对象上。那么如果不想在对象内部包含函数引用,而想在某个对象上强制调用函数,那么就需要显示绑定。

硬绑定是一种显式的强制绑定,使用 apply() 或者是 call() 改变函数的调用对象(将另一个对象作为参数调用对象方法)。第一个参数就表示改变后的调用这个函数的对象。此时,this 指的就是这第一个参数。也就是说,将所用方法指定绑定到目的对象中

如果传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对象,这个原始值会被转换成其对象形式(也就是 new String(..)、new Boolean(..) 或者 new Number(..))。这通常被称为“装箱”。

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
// 硬绑定的典型应用场景就是创建一个包裹函数,传入所有的参数并返回接收到的所有值.
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
// 硬绑定call与apply皆可
var zswant = {
	fruit: 'coco',
	sayName: function() {
		console.log('zs想吃' + this.fruit)
	}
} 
var fruit1= {
	fruit: 'watermelon'
}
var fruit2 = {
	fruit: 'Strawberry'
}
// call调用对象的方法
zswant.sayName.call(fruit1) // zs想吃watermelon
zswant.sayName.apply(fruit2) // zs想吃Strawberry

注意这里 apply()call() 的参数为空、null 和 undefined 时,默认传入全局对象。且由于硬绑定是一种非常常用的模式,所以在 ES5 中提供了内置的方法 Function.prototype. bind。

globalThis.fruit = 'hahaha'
// 硬绑定call与apply皆可
var zswant = {
	fruit: 'coco',
	sayName: function() {
		console.log('zs想吃' + this.fruit)
	}
} 
var fruit1= {
	fruit: 'watermelon'
}
var fruit2 = {
	fruit: 'Strawberry'
}
// call调用对象的方法
zswant.sayName.call() // zs想吃watermelon
zswant.sayName.apply() // zs想吃Strawberry

new 绑定

在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用 new 初始化类时会调用类中的构造函数。通过构造函数生成新的对象。那么 this 指这个新对象。

首先重新定义一下 JavaScript 中的“构造函数”。在 JavaScript 中,构造函数只是一些使用 new 操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,只是被 new 操作符调用的普通函数而已。——《你不知道的 javascript 上卷》

使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行[[原型]]连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function dev (name) {
	this.name = 'zs'; // 注意这里的不同
	this.sayName = function () {
		console.log('dev是' + this.name)
	}
}
var name = "zsxzy" // 即使有同名变量
var zy = new dev('zss')
zy.sayName(); // dev是zs
function dev (name) {
	this.name = name; // 构造函数实例化传参且传入
	this.sayName = function () {
		console.log('dev是' + this.name)
	}
}
var name = "zsxzy" // 即使有同名变量
var zy = new dev('zss') // 输入进行变量实例化,这时this与实例化后新对象捆绑
zy.sayName(); // dev是zss

箭头函数绑定

箭头函数的 this 对象,就是外部环境所在的作用域指向的对象,而不是使用时所在的作用域指向的对象,即并不是由自身决定(并不属于自身)的。

var name = 'window'; 
var A = {
name: 'A',
sayHello: () => {
    console.log(this.name)
    }
}
A.sayHello(); // window
var name = 'window';
var A = {
    name: 'A',
    sayHello: function(){
        return arrow = () => console.log(this.name) // 返回箭头函数
    }
}
var sayHello = A.sayHello();
sayHello(); // A 
var B = {
   name: 'B'
}
sayHello.call(B); // A
sayHello.apply(); // A
// sayHello.bind() 不会输出结果 => bind方法返回的是綁绑定 this 后的原函数
sayHello.bind()(); // A

箭头函数在定义时就完成了 this 绑定,执行时调用位置、用 call()apply()bind() 都无法更改。

经典题

this 相关

function foo(el) { console.log( el, this.id ); }
var obj = { id: "okk" };
// forEach(callback(currentValue [, index [, array]])[, thisArg])
[1, 2, 3].forEach( foo, obj ); // 1 okk  2 okk  3 okk
var A = function( name ){ this.name = name; };
var B = function(){ A.apply(this, arguments); };
B.prototype.getName = function(){ return this.name; };
var b = new B('okk');
console.log( b.getName() ); // okk

手写 Bind、Call、Apply

bind 被调用时,会创建一个新函数,这个新函数的 this 被指定为 bind 的第一个参数,余下的参数将作为新函数的参数,供调用时使用。

绑定函数也可以使用 new 运算符构造,其会表现为目标函数已经被构建完毕。提供的 this 值会被忽略,但前置参数仍会提供给模拟函数。

Function.prototype.myBind = function (ctx = globalThis) {
  // bind 传入 this 和 args
  const fn = this; // myBind 方法是被待改变 this 指向的函数调用
  const args = Array.from(arguments).slice(1); // Array.prototype.slice.call(arguments, 1) 也可
  const newFunc = function () {
    const newArgs = args.concat(...arguments); // 考虑到 bind 实际具有柯里化特性 (arg01)(arg02)
    if (this instanceof newFunc) {
      // instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
      fn.apply(this, newArgs); // 对调用 bind 函数产生的新函数实例化 new 的情况
    } else {
      fn.apply(ctx, newArgs);
    }
  };
  newFunc.prototype = Object.create(fn.prototype); // 将实例与原型对象关联 => 可访问待改变 this 指向函数原型链上的属性
  return newFunc;
};
/* Test Code */
const me = {name: "ok"}, you = {name: "okk"};
function say(){console.log(`${this.name || 'default'}`)}
say.prototype.associationPropertiesTest = "testOk"
const meSay = say.myBind(me), youSay = say.myBind(you);
meSay();
youSay();
const mesay = new meSay();
console.log(mesay.associationPropertiesTest);

call 方法使用一个指定的 this 和单独给出的一个或多个参数来调用一个函数。

Function.prototype.myCall = function (ctx = globalThis) {
  // ctx 调用方法触发 this 绑定为 ctx
  const key = Symbol("key");
  ctx[key] = this;
  const args = [...arguments].slice(1);
  const res = ctx[key](...args);
  delete ctx[key];
  return res;
};
/* Test Code */
const me = { name: "ok" };
function say() { console.log(`${this.name || "default"}`); }
say.myCall(me);

apply 方法调用一个具有给定 this 值的函数,以及以一个数组(或一个类数组对象)的形式提供的参数。

Function.prototype.myApply = function(ctx = globalThis){ // ctx 调用方法触发 this 绑定为 ctx
  const key = Symbol("key");
  ctx[key] = this;
  let res;
  if(arguments[1]){
    res = ctx[key](...arguments[1]);
  } else {
    res = ctx[key]()
  }
  delete ctx[key];
  return res;
}
/* Test Code */
const me = { name: "ok"}
function say(){ console.log(`${this.name || "default"}`) };
say.myApply(me)

总结

  • this 是 JavaScript 中的一个关键字,是函数运行时所在的环境对象
  • 根据使用场合存在不同的 this 绑定方式,但是在执行期间不能被赋值。此外,需要注意严格模式和非严格模式之间的使用差别
  • call、apply、bind 都可以改变函数的 this 指向
  • call、apply、bind 首个参数都是 this 要指向的对象,若没有这个参数或参数为 undefined 或 null,则默认指向全局 window
  • call、apply、bind 都可以传参,但 apply 是数组,call 是参数列表,apply 和 call 是一次性传入参数,而 bind 可以分为多次传入
  • bind 是返回绑定 this 之后的函数,便于稍后调用,而 apply、call 则是立即执行

结束

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!