深入理解 this 关键字,以及其背后的相关概念
调用位置就是函数在代码中被调用的位置(而不是声明的位置)。调用位置决定了 this 的绑定
比如下面代码:
jsfunction baz() {
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域
console.log("baz");
bar(); // <-- bar 的调用位置
}
function bar() {
// 当前调用栈是 baz -> bar
// 因此,当前调用位置在 baz 中
console.log("bar");
foo(); // <-- foo 的调用位置
}
function foo() {
// 当前调用栈是 baz -> bar -> foo // 因此,当前调用位置在 bar 中
console.log("foo");
}
baz(); // <-- baz 的调用位置
可以使用浏览器的开发工具查看调用栈
![]() | ![]() | ![]() |
---|
this的指向有以下四条特性/规则
当函数被独立调用时,函数的 this 指向 window
独立调用就是像这样:
foo()
的调用
比如
jsfunction foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2
如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此 this 会绑定到 undefined
当函数引用有上下文对象 且被该对象调用时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象
jsfunction foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
// 这里就是隐式绑定,foo函数的this绑定到了obj上
需要注意的是,对象属性引用链中只有最顶层或者说最后一层会影响调用位置
比如
jsfunction foo() {
console.log(this.a);
}
var obj2 = {
a: 42,
foo: foo,
};
var obj1 = {
a: 2,
obj2: obj2,
};
obj1.obj2.foo(); // 42
// 相当于 obj2.foo(),因为只有最后一层会影响调用位置
隐式绑定的函数可能会丢失绑定对象,也就是说它会应用默认绑定
隐式丢失的几种情况:
jsfunction foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo,
};
// 注意这里的函数别名,会导致隐式绑定丢失,导致foo函数的this指向全局
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"
jsfunction 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"
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子一样
fn.call(obj, x, x, ...)
将fn函数的this指向obj,并调用,call的剩余参数是fn需要的参数javascriptfunction foo(aram) {
console.log("foo", param.a); // foo 1
function bar(x, y) {
console.log("bar", this.a, x, y); // bar 1 2 3
}
bar.call(param, 2, 3);
}
foo({ a: 1 });
fn.apply(obj, [x, x, ...])
将fn函数的this指向obj,并调用,apply的第二个参数是一个数组javascriptfunction foo(param) {
console.log("foo", param.a); // foo 1
function bar(x, y) {
console.log("bar", this.a, x, y); // bar 1 2 3
}
bar.apply(param, [2, 3]);
}
foo({ a: 1 });
fn.bind(obj, x, x, ...)(x, x, ...)
将fn函数的this指向obj,bind会返回一个新的函数,新的函数也可以传递参数javascriptfunction foo(param) {
console.log("foo", param.a); // foo 1
function bar(x, y) {
console.log("bar", this.a, x, y); // bar 1 2 3
}
return bar.bind(param, 2);
}
const bar = foo({ a: 1 });
bar(3);
new 绑定 - this会绑定到新创建的对象上
javascriptfunction Person(name, age) {
// this 指向新创建的对象
this.name = name;
this.age = age;
// 通过new调用构造函数时,this指向新创建的对象
// 直接调用构造函数时,应用默认绑定规则,this指向全局或undefined
console.log(this);
}
// 使用 new 关键字调用构造函数
const person1 = new Person("张三", 25);
console.log(person1.name); // "张三"
console.log(person1.age); // 25
// 不使用 new 调用,this 会指向全局对象(非严格模式)或 undefined(严格模式)
const person2 = Person("李四", 30); // this 不会指向新对象
console.log(person2); // undefined (因为没有显式返回)
jsvar a = 1;
function foo() {
var obj1 = {
a: 2,
bar: function () {
console.log("bar", this.a);
var obj2 = {
a: 3,
baz: () => {
console.log("baz", this.a);
},
};
// 箭头函数不会创建自己的 this,它会捕获外层函数的 this
obj2.baz(); // baz 2
},
};
console.log("foo", this.a);
obj1.bar(); // bar 2
}
foo(); // foo 1
按照上述 new 调用规则
而我们又知道,新对象是由构造函数实例化的,对象隐式原型的constuctor
就是构造函数
或者简单说,新对象 instanceof 构造函数
也就是 this instaceof 构造函数
除了这种方法,ES6中引入了new.target
也可以判断当前函数是否被new
JavaScriptfunction Fn() {
if (new.target) {
console.log("new new.target 触发");
}
if (this instanceof Fn) {
console.log("new instanceof 触发");
} else {
console.log("正常执行");
}
}
Fn();
new Fn();
改变this指向核心是使用隐式绑定规则,手写时主要思路是如何让函数调用时上下文为指定对象。
这里只写call
的实现,apply
只是传递参数的方式区别
JavaScriptFunction.prototype.myCall = function (obj, ...args) {
// 指定上下文对象,如果没传的话,默认是全局对象
const ctx = obj || globalThis;
// 将 函数绑定到 上下文对象上,这样就可以使用隐式绑定规则
// ctx.fn = this;
// 或者可以使用 Symbol 生成一个独一无二的 key,防止干扰 原对象
const key = Symbol("fn");
ctx[key] = this;
// 调用并记录返回值,后续需要删除上下文上的 fn
// const res = ctx.fn(...args);
const res = ctx[key](...args);
// 删除 函数,防止干扰上下文对象
// delete ctx.fn;
delete ctx[key];
return res;
};
bind返回一个新的函数,这个函数在以下特性上需要和原函数保持一致
JavaScriptFunction.prototype.myBind = function (obj, ...args1) {
// 使用闭包特性保留 this,也就是函数
const fn = this;
function wrap(...args2) {
const isNew = this instanceof wrap;
// 如果是new调用时,应该把this传给原函数,让原函数处理this,相当于构造函数中执行的赋值
const ctx = isNew ? this : obj;
return fn.apply(ctx, [...args1, ...args2]);
}
// 保持原型链
if (fn.prototype) {
wrap.prototype = Object.create(fn.prototype);
wrap.prototype.constructor = wrap;
}
return wrap;
};
本文作者:pepedd864
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!