JavaScript函数的 this 的值,
通常在全局执行环境中(在任何函数体外部) 都指向全局对象(浏览器中是window),在函数中取决于调用方式
下面是this的几种绑定规则,搞清楚了在日常开发中方便我们写出简洁的代码、防止因为搞不清楚this写出bug
默认绑定
默认绑定全局对象window
//普通函数被独立调用
function bar() {
console.log("bar:", this)
}
bar() //bar:Window
//函数定义在对象中,但是被独立调用
var obj = {
name: "CR7",
bar: function() {
console.log("bar:", this)
}
}
var baz = obj.bar
baz() //bar:Window
//高阶函数的调用
var obj = {
name: "CR7",
bar: function() {
console.log("bar:", this)
}
}
function highFn(fn) {
fn()
}
highFn(obj.bar) //bar:Window
隐式绑定
函数被调用时,它的前面确实加上了对某个对象的引用。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
//当foo()被调用时,它的前面确实加上了对obj的引用
对象属性引用链中只有上一层或者说最后一层在调用位置中起作用,例如:
function foo() {
console.log(this.a);
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
//obj2作为foo调用位置的最后一层,foo的this指向obj2
隐式丢失,看似绑定了对象,实则属于默认绑定规则,例如:
//函数定义在对象中,但是被独立调用
var obj = {
name: "CR7",
bar: function() {
console.log("bar:", this)
}
}
var foo = obj.bar
foo() //bar:Window
//虽然foo是obj.bar的一个引用,但是实际上,它引用的是bar函数本身,
//因此此时的goo()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
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"
显示绑定
通过函数原型上的方法 call 和 apply 实现this的显示绑定,call 和 apply区别只是传参方式不一样
const obj = {
name: "CR7",
};
function foo() {
console.log(this);
}
foo.call(obj); // {name: 'CR7'}
foo.call(123); // Number {123}
foo.call("abc"); // String {'abc'}
foo.call(true); // Boolean {true}
//如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作this的绑定对象,
//这个原始值会被转换成它的对象形式(也就是new String(..)、new Boolean(..)或者new Number(..))
通过 bind 方法固定this绑定,解决把对象中的函数属性取出来单独调用导致的绑定丢失问题
function foo(name, age) {
console.log(this); // {name: 'why'}
console.log("参数:", name, age); //参数: CR7 Messi
}
const obj = { name: "why" };
const bar = foo.bind(obj, "CR7");
bar("Messi");
// foo的this固定为obj对象,无论bar怎么调用
// 同时foo的参数是由bind函数第二个参数及以后的参数加上bar的参数组成
JavaScript中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和bind一样,确保你的回调函数使用指定的this。
call/apply常用于框架、组件封装
// vue2.7.8 /src/core/instance/state.ts
function initData(vm: Component) { //vm就是当前组件vue实例
let data: any = vm.$options.data //拿到我们写的data
data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
//......
}
export function getData(data: Function, vm: Component): any {
pushTarget()
try {
return data.call(vm, vm) //如果data是个函数,vue调用data,绑定当前实例作为this,并传入当前实例作为参数
} catch (e: any) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
// vue2.7.8 /src/core/observe/watcher.ts
export default class Watcher implements DepTarget {
// ......
constructor(
vm: Component | null, //Vue实例
expOrFn: string | (() => any),
cb: Function, //我们平时在watch中写的回调函数
options?: WatcherOptions | null,
isRenderWatcher?: boolean //是否为渲染函数的观察者
) {
//......
}
// ......
run() {
this.cb.call(this.vm, value, oldValue)
//vue会call调用我们传入的回调函数,绑定当前实例为它的this,并把新值和旧值传给回调函数
}
}
new 绑定
包括内置对象函数(比如Number(),String())在内的所有函数都可以用new来调用,这种函数调用被称为构造函数调用。这里有一个重要但是非常细微的区别:实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”, ES6 的 class 可以通过new 实例化一个类,但也只是普通函数new调用的语法糖
使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
1.创建(或者说构造)一个全新的对象。
2.)这个新对象内部的[[Prototype]]指针被赋值为构造函数的prototype属性
3.这个新对象会绑定到函数调用的this。
4.如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function foo(a) {
this.a = a;
}
var bar = new foo(2); //foo中的this指向a
console.log(bar.a); //2
class Person {
constructor() {
this.name = "CR7";
this.age = "37";
}
}
const p = new Person(); // Person类中的this指向 p
console.log(p.name, p.age); // CR7 37
箭头函数
箭头函数的this是根据外层(函数或者全局)作用域决定的,不适用上面4条规则
箭头函数 没有自己的this,arguments,super 或 new.target ,箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数,不能使用new调用
const foo = () => {
console.log("foo的this:", this);
};
foo(); // foo的this: Window
foo.apply("CR7"); // foo的this: Window
// foo 的上层作用域是全局作用域,this指向window
const obj = {
name: "CR7",
foo: () => {
const bar = () => {
console.log("bar中的this:", this); // bar中的this:Window
};
return bar;
},
};
const fn = obj.foo();
fn.apply("bbb");
// fn等于箭头函数 bar, 执行fn.apply("bbb"),箭头函数不适用显示绑定,往上层作用域找this,
//bar上层作用域是foo函数作用域,foo是箭头函数,继续找到foo的上层作用域全局作用域,
//obj对象不是作用域哈
// 下面看一个vue实际开发例子
export default {
data() {
return {
name:"CR7"
},
methods:{
doSomething(){
console.log(this.name) //CR7
},
onSubmit() {
this.$refs.ruleForm.validate(valid => { //这是一个form表单校验方法,来自ant-design-vue
/**
在这个回调函数中要拿到vue实例中数据,比如this.name、this.doSomething,就必须使用箭头函数,
this才能绑定到上层onSubmit函数作用域的this,onSubmit的this是绑定到当前vue实例上的
*/
});
},
}
}
上面这个例子涉及到一个参数作用域的问题:如果函数参数没有默认值,就只有一个函数作用域,如果函数参数有默认值,函数的参数就会形成一个作用域,保存参数的值,上面例子中的回调函数和validate同一个函数作用域,如果不使用箭头函数,this就不能指向vue实例
优先级
通过以上四条规则,我们已经知道默认绑定的优先级最低,下面比较一下隐式绑定、显示绑定、new绑定
function foo() {
console.log(this.name);
}
const obj = { foo, name: "CR7" };
obj.foo(); //CR7
obj.foo.apply({ name: "Neymar" }); //Neymar
obj.foo.call({ name: "Messi" }); //Messi
//call 、apply显示绑定优先于隐式绑定
const obj = {
name: "CR7",
foo: function () {
console.log(this); //foo {}
console.log(this === obj); //false
},
};
new obj.foo();
// foo 的this不是obj
const obj = {
foo: function (name) {
this.name = name;
},
};
obj.foo("CR7");
console.log(obj.name); //CR7 foo的this绑定为obj
const f = new obj.foo("Messi");
console.log(f.name); //Messi //foo的this绑定为新对象f
//new绑定优先于隐式绑定
// new和call/apply无法一起使用
function foo() {
console.log(this);
}
new foo.call({ name: "CR7" })
// Uncaught TypeError: foo.call is not a constructor
//但是new 和 bind 可以比较
function foo() {
console.log(this);
}
const bar = foo.bind({ name: "CR7" });
bar(); // {name: 'CR7'}
new bar(); // foo {} //this为new创造的新对象
// new优先级高于bind
function foo() {
console.log(this); //String {'aaa'}
}
const bar = foo.bind("aaa");
bar.call("bbb");
// call调用没有改变bar函数的this
所以一般情况下优先级new > bind > call/apply >隐式绑定 > 默认绑定
this练习
var name = "CR7";
function Person(name) {
this.name = name;
this.obj = {
name: "Messi",
foo1: function () {
return function bar1() {
console.log(this.name);
};
},
foo2: function () {
return () => {
console.log(this.name);
};
},
};
}
var person1 = new Person("person1");
var person2 = new Person("person2");
person1.obj.foo1()(); //"CR7"
// person1.obj.foo1()返回bar1独立调用,默认绑定
person1.obj.foo1.call(person2)(); // "CR7"
// person1.obj.foo1.call(person2)还是返回bar1独立调用,默认绑定
person1.obj.foo1().call(person2); //person2
// person2绑定为返回函数bar1的this
person1.obj.foo2()(); //Messi
// person1.obj.foo2()返回箭头函数,它的this由上层foo2决定,foo2隐式绑定为obj
person1.obj.foo2.call(person2)(); //person2
// foo2的this绑定为person2再返回箭头函数,箭头函数的this也为上层foo2的this
person1.obj.foo2().call(person2); //Messi
//foo2返回箭头函数,call绑定对箭头函数不管用,还是看上层foo2, foo2隐式绑定为obj
var name = "CR7";
function Person(name) {
this.name = name;
this.foo1 = function () {
console.log(this.name);
};
this.foo2 = () => console.log(this.name);
this.foo3 = function () {
return function bar() {
console.log(this.name);
};
};
this.foo4 = function () {
return () => {
console.log(this.name);
};
};
}
var person1 = new Person("person1");
var person2 = new Person("person2");
person1.foo1(); // person1 隐式绑定person1
person1.foo1.call(person2); //person2 显示绑定person2
person1.foo2(); // person1
person1.foo2.call(person2); // person1
// foo2是箭头函数,隐式绑定和显示绑定都不管用,找上层作用域Person,Person中的this就是person1
person1.foo3()(); // CR7
person1.foo3.call(person2)(); // CR7
//foo3返回bar函数到最外层独立调用,默认绑定全局
person1.foo3().call(person2); // person2 foo3返回bar函数显式绑定person2
person1.foo4()(); //person1
person1.foo4.call(person2)(); // person2
person1.foo4().call(person2); // person1
//foo4无论如何调用,返回一个箭头函数,箭头函数无论如何调用,它的this是上层foo4函数的this
var name = "CR7";
var person1 = {
name: "person1",
foo1: function () {
console.log(this.name);
},
foo2: () => console.log(this.name),
foo3: function () {
return function bar() {
console.log(this.name);
};
},
foo4: function () {
return () => {
console.log(this.name);
};
},
};
var person2 = { name: "person2" };
person1.foo1(); // 隐式绑定person1
person1.foo1.call(person2); // 显式绑定person2
person1.foo2(); //CR7
person1.foo2.call(person2); //CR7
//foo2是箭头函数,无论如何调用,this由上层作用域(这里是全局,不是person1对象)
person1.foo3()(); // 默认绑定CR7
person1.foo3.call(person2)(); // 默认绑定 CR7
person1.foo3().call(person2); // 显式绑定 person2
// foo3无论怎样调用,返回函数bar,关键看bar如何调用
person1.foo4()(); // person1 foo4隐式绑定person1
person1.foo4.call(person2)(); // person2 foo4显示绑定person2
person1.foo4().call(person2); // person1 foo4隐式绑定person1
//foo4返回一个箭头函数,无论这个箭头函数如何调用,关键看上层foo4的this
参考:
《你不知道的JavaScript(上卷)》第2章