this是什么?
this在JavaScript中一个非常重要的概念,也是特别令人迷惑的。this是什么?首先记住this不是指向自身!this 就是一个指针
,总是指向最后调用它的对象,即代表着它的直接调用者。
普通函数中this
默认绑定(全局作用域)
- 默认情况
var name = 'xiaoming';
function hello() {
console.log('Hello,', this.name);
//window:Hello, xiaoming;
//node:Hello, undefined
}
hello();
- 严格模式
var name = 'xiaoming';
function hello() {
'use strict';
console.log('Hello,', this.name);
//TypeError: Cannot read property 'name' of undefined
}
hello();
直接调用hello()时,应用了默认绑定,在全局作用域/全局环境中,this指向的就是全局变量
- 在浏览器里,指向window对象
- 在Node.js里,指向global对象
- 严格模式下,this指向undefined
隐式绑定(对象方法)
函数的调用是在某个对象上触发的,即调用位置上存在
上下文对象
。典型的形式为 XXX.fun()
当一个函数被调用时,应该立马看()左边的部分。
- 如果()左边是一个引用,那么,函数的this指向的就是这个
引用所属的对象
- 否则this指向的就是全局对象(默认绑定)
var name = 'country'
const china = {
name: 'china',
year: 1949,
describe() {
console.log(`${this.name} was built in ${this.year}`);
},
details: {
currency: 'RMB',
printDetails() {
console.log(`the currency is ${this.currency}`);
},
},
};
china.describe();
// china was built in 1949
// ()左边的describe属于china,describe里的this指向china
china.details.printDetails();
// the currency is RMB
// ()左边是printDetails,printDetails属于china.details,所以this就是china.details
var describe = china.describe;
describe();
// country was built in undefined
//执行describe(),()左边是describe,这个时候this执行的是默认绑定,this指向的是全局对象window.这就是隐式绑定丢失
如果代码都那么简单,那么this的指向也就简单明了了。来点复杂点的看看:
const obj = {
name: 'spike',
friends: ['deer', 'cat'],
loop: function() {
this.friends.forEach( // 这个this指向obj
function( friend ) {
console.log(`${this.name} knows ${friend}`);
console.log(this === window); // 在浏览器环境下,全局对象为window
}
)
}
}
obj.loop();
// ()左边是loop,属于obj,所以loop函数中的this指向obj
输出
$ node test
undefined knows dear
true
undefined knows cat
true
可以看到,在forEach中的this并不是期待的那样指向obj,而是指向全局对象了
可以用上面提到的,还是看()左边,在forEach中,()左边是function,而不是一个引用, 匿名函数
没有直接调用者,不属于任何对象,他不是一个对象的方法,在浏览器环境中他的this指向window
我们来看下面一个例子:
function sayHi(){
console.log('Hello,', this.name);
}
var person1 = {
name: 'john',
sayHi: function(){
setTimeout(function(){
console.log('Hello,',this.name);
})
}
}
var person2 = {
name: 'Christina',
sayHi: sayHi
}
var name='Wiliam';
person1.sayHi();
// Hello, Wiliam
// setTimeout的回调匿名函数没有直接调用者,this使用的是默认绑定,非严格模式下,执行的是全局对象
setTimeout(person2.sayHi,100);
// Hello, Wiliam
// setTimeout(fn,delay){ fn(); },相当于是将person2.sayHi赋值给了一个变量,最后执行了变量,这个时候,sayHi中的this显然和person2就没有关系了。
setTimeout(function(){
person2.sayHi();
},200);
// Hello, Christina
// 这是执行的是person2.sayHi()使用的是隐式绑定,因此这是this指向的是person2,跟当前的作用域没有任何关系。
再看一个 arguments 的例子:
在使用obj.foo(temp)时,将temp函数当成了参数传递到foo中,把一个函数当成参数传递到另一个函数的时候,会发生隐式丢失的问题,且与包裹着它的函数的this指向无关。在非严格模式下,会把该函数的this绑定到window上,严格模式下绑定到undefined
传入进去的temp会被arguments所搜集,所以可以使用arguments0这样的形式调用。而arguments是一个类数组,数组或者类数组的调用下标就像是对象调用属性一样,因此此时temp内的this就是arguments了,而在arguments内是没有age这个属性的,所以会打印出undefined
扩展
JS(ES5)里面有三种函数调用形式:
func(p1, p2)
obj.child.method(p1, p2)
func.call(context, p1, p2) // 先不讲 apply
其实第三种调用形式,才是正常调用形式,其他两种都是语法糖
,可以等价地变为 call 形式,this就是第三种形式中的 context参数
。
举几个「转换代码」示例:
1、func(p1, p2) 等价于
//func.call(undefined, p1, p2),fun普通函数里头的this 就是 undefined在浏览器中是window,严格模式下是undefined
// 如果你希望这里的 this 不是 window,很简单:
func.call(obj) // 那么里面的 this 就是 obj 对象了
2、obj.foo() // 转换为 obj.foo.call(obj),foo普通函数里头的this 就是 obj
3、obj.child.method(p1, p2)
//等价于obj.child.method.call(obj.child, p1, p2)
4、arr[0]()
//假想为 arr.0()
//然后转换为 arr.0.call(arr)
//那么里面的 this 就是 arr
this 就是你 call 一个函数时,传入的第一个参数。
绑定优先级
如果同时应用了多种规则,怎么办?
显然,我们需要了解哪一种绑定方式的优先级更高,这四种绑定的优先级为:
new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
显式绑定(call,apply,bind)
显式绑定比较好理解,就是通过call,apply,bind
的方式,显式的指定this所指向的对象。
call,apply和bind的第一个参数,就是对应函数的this所指向的对象,本质都是改变 this 的指向。不同点 call、apply 是直接调用函数
,bind 是返回一个新的函数。call和apply都会执行对应的函数,而bind方法需要手动调用
。call和apply的作用一样,只是传参方式不同。
使用 call,apply
function sayHi() {
console.log('Hello,', this.name);
}
var person = {
name: 'john',
sayHi: sayHi,
};
var name = 'Wiliam';
var Hi = person.sayHi;
Hi();
// Hello, Wiliam
// 默认绑定,this指向window
Hi.call(person); //Hi.apply(person)
// Hello, john
// 因为使用明确将this显式绑定在了person上。
然而使用显示绑定也会出现隐式绑定所遇到的绑定丢失
function sayHi() {
console.log('Hello,', this.name);
}
var person = {
name: 'john',
sayHi: sayHi,
};
var name = 'Wiliam';
var Hi = function (fn) {
fn();
};
Hi.call(person, person.sayHi);
// Hello, Wiliam
Hi.call(person, person.sayHi)的确是将this绑定到Hi中的this了,但是在执行fn的时候,person.sayHi已经被赋值给fn了。相当于直接调用了sayHi方法,()左边是sayHi,对应的是默认绑定。
如果希望绑定不会丢失,要怎么做?很简单,调用fn的时候,也给它显式绑定。
function sayHi(){
console.log('Hello,', this.name);
}
var person = {
name: 'john',
sayHi: sayHi
}
var name = 'Wiliam';
var Hi = function(fn) {
fn.call(this);
}
Hi.call(person, person.sayHi);
// Hello, john
// 因为person被绑定到Hi函数中的this上,fn又将这个对象绑定给了sayHi的函数。这时,sayHi中的this指向的就是person对象。
快速记忆
call和apply两个函数用法差不多,都是劫持另外一个对象的属性和函数,区别只在参数的写法上面。但是恰恰是这个区别,很多人就记不住,所以有了下面这个技巧。
- call
打电话
一个一个输入号码 ,参数一个一个传 - apply
数组
的英文array
首字母都是a ,传数组。
var obj = {
num1: 1
}
function count(num2, num3) {
console.log(this.num1 + num2 + num3);
}
count.call(obj, 1, 1); // 3
count.apply(obj, [1, 1]); // 3
和bind的区别
将刚刚的例子使用 bind 试一下
var obj = {
num1: 1
}
function count(num2, num3) {
console.log(this.num1 + num2 + num3);
}
count.bind(obj, 1, 1);
我们会发现并没有输出,这也应了前面说的bind需要手动调用
var obj = {
num1: 1
}
function count(num2, num3) {
console.log(this.num1 + num2 + num3);
}
count.bind(obj, 1, 1)(); // 3
如果我们将null或者是undefined作为this的绑定对象传入call、apply或者是bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
var foo = {
name: 'Selina'
}
var name = 'Chirs';
function bar() {
console.log(this.name);
}
bar.call(null); //Chirs
new 绑定(构造函数)
参考JavaScript高级程序设计,我们可以知道new操作符做了四件事:
- 创建一个新对象
- 将构造函数的作用域赋给了新对象(因此this指向了该对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象
因此,我们使用new来调用函数的时候,就会新对象绑定到这个函数的this上。
var _that;
function Country(name, year) {
this.name = name;
this.year = year;
// 保存构造函数中的this
_that = this;
console.log(`${this.name} was built in ${this.year}`); // China was built in 1949
}
// 通过new关键字执行构造函数
var china = new Country('China', 1949);
// 构造函数中的this指向的就是新创建的对象实例china
console.log(_that === china); // true
如果你没有用new关键字去执行构造函数,那么就要分析函数被调用时所属的作用域了
function Point(x, y) {
this.x = x;
this.y = y;
}
var p = Point(7, 5); // 没有用new关键字去执行构造函数!
console.log(p === undefined);
// true
//没有用new,所以构造函数没有返回一个实例对象, 所以p === undefined
// 没有用new关键字,Point(7,5);就只是把函数执行了一遍
// ()左边是Point,属于全局对象,所以this指向全局对象
console.log(x); // 7
console.log(y); // 5
DOM绑定事件处理器(event handler)中this的指向
<div id="test">I am an element with id #test</div>
function doAlert() {
alert(this.innerHTML);
}
doAlert(); // undefined
// doAlert()属于全局对象
var myElem = document.getElementById('test');
myElem.onclick = doAlert;
alert(myElem.onclick === doAlert); // true
myElem.onclick(); // I am an element
// ()左边是onclick也就是doAlert,属于myElem,所以this指向myElem
哪个元素触发事件,this就指向哪个元素
小结
以上,对于普通函数中的this,通过查看()左边所属的对象去确定,真的很好用。强烈推荐call语法糖
的代码转换方式👍一眼就能看懂
实质上,this是在创建函数的执行环境时,在创建阶段确定的,因此,弄透执行环境,去思考执行环境创建阶段的this的指向,this的指向就不会弄错了吧。
原理
摘自:阮一峰老师
原文:JavaScript 的 this 原理
原文地址:http://www.ruanyifeng.com/blog/2018/06/javascript-this.html
取其中的函数部分讲
var obj = { foo: function () {} };
JavaScript 引擎
会先在内存里面,生成一个对象,然后把这个对象的内存地址赋值给变量obj,原始的对象以字典结构
保存,每一个属性名都对应一个属性描述对象
(图中黄色部分展示foo属性的值保存在属性描述对象的value属性里面)。JavaScript引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo属性的value属性。
由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。
var f = function () {
console.log(this.x);
}
var x = 1;
var obj = {
f: f,
x: 2,
};
// 单独执行
f() // 1
// obj 环境执行
obj.f() // 2
函数体里面的this.x就是指当前运行环境的x。上面代码中,函数f在全局环境执行,this.x指向全局环境的x
在obj环境执行,this.x指向obj.x。
obj.foo()
是通过obj
找到foo
,所以就是在obj
环境执行。一旦var foo = obj.foo
,变量foo
就直接指向函数本身,所以foo()
就变成在全局环境执行。
箭头函数中this
实际上箭头函数里并没有 this,如果你在箭头函数里看到 this,你直接把它当作箭头函数外面的 this 即可。
外面的 this
是什么,箭头函数里面的 this 就还是什么,因为箭头函数本身不支持 this
,箭头函数内外 this 就是同一个东西
。
- 默认指向定义它时,所处
上下文的对象
的this指向。即ES6箭头函数里this的指向就是上下文里对象this指向,偶尔没有上下文对象,this就指向window - 即使是call,apply,bind等方法也不能改变箭头函数this的指向
a(() => {
console.log(this);
});
obj.a(() => {
console.log(this);
});
//这两个this直接就是他的函数作用域window了,不用管后面传入到哪里去
const obj = {
num: 10,
hello: function () {
console.log(this); // obj
setTimeout(() => {
console.log(this); // obj
});
}
}
obj.hello()//obj.hello.call(obj) hello的作用域是obj
//setTimeout里头箭头函数指向的就是他的上级函数作用域hello的this
const obj = {
radius: 10,
diameter() {
return this.radius * 2;
// 普通函数,里面的this指向直接调用它的对象obj。
},
perimeter: () => 2 * Math.PI * this.radius,
// 箭头函数,this应该指向上下文函数this的指向,这里上下文没有函数对象,就默认为window,而window里面没有radius这个属性,就返回为NaN。
};
console.log(obj.diameter()); // 20
console.log(obj.perimeter()); // NaN
小结
箭头函数的this在定义的时候就确定
,逐级向上查找找到最近的函数作用域的this,直到window
手写new
// Con:构造函数
// ...args:其余参数
function create(Con, ...args) {
let obj = Object.create(Con.prototype);
let result = Con.apply(obj, args);
return result instanceof Object ? result : obj;
}
- 内部创建一个空对象
obj
- 使用
Object.create()
方法创建一个新对象,使用现有的对象Con
来提供新创建的对象的__proto__。让obj 对象可以访问到构造函数原型链上的属性,这段代码等同于let obj = {}; obj.__proto__ = Con.prototype
Con.apply(obj, args)
将 obj 绑定到构造函数上,将构造函数的作用域赋给了新对象,并且传入剩余的参数,为这个新对象添加属性- 判断构造函数返回值是否为对象,如果为对象就使用构造函数返回的值,否则使用 obj,这样就实现了忽略构造函数返回的原始值
测试:
function create(Con, ...args) {
let obj = Object.create(Con.prototype);
let result = Con.apply(obj, args);
return result instanceof Object ? result : obj;
}
function Test(name, age) {
this.name = name;
this.age = age;
}
Test.prototype.sayName = function () {
console.log(this.name);
};
const a = create(Test, 'JohnYu', 27);
console.log(a.name); // 'JohnYu'
console.log(a.age); // 27
a.sayName(); // 'JohnYu'
关于原型和原型链可以参考我的另一篇文章:🎉👨👩👧👧图解Javascript原型
手写call
Function.prototype.myCall = function (context, ...args) {
// 1.如果有对象则this指向对象,否则this指向window
var context = context || window;
// 2.mycall方法的调用者是bar,this就是bar
// 把调用者bar函数命名为fn,被foo对象或者window所拥有的
context.fn = this;
// 3.call后面的参数分别传到fn或者window所拥有的bar函数中,变成调用obj.bar('JohnYu', 27),此时bar函数内this指向foo
var result = context.fn(...args);
delete context.fn;
return result;
};
value = 2;
let foo = {
value: 1,
};
let bar = function (name, age) {
console.log(name, age, this.value);
};
bar.myCall(foo, 'JohnYu', 27); //JohnYu 27 1
bar.myCall(null, 'JohnYu', 27); //JohnYu 27 2
手写apply
apply
与call
思路一致,只是对参数进行不同处理即可:
Function.prototype.myAplly = function (context, args) {
var context = context || window;
context.fn = this;
var result = context.fn(...args);
delete context.fn;
return result;
};
value = 2;
let foo = {
value: 1,
};
let bar = function (name, age) {
console.log(name, age, this.value);
};
bar.myAplly (foo, ['JohnYu', 27]); //JohnYu 27 1
bar.myAplly (null, ['JohnYu', 27]); //JohnYu 27 2
手写bind
Function.prototype.myBind = function (context) {
// 判断是否是一个函数
if (typeof this !== 'function') {
throw new TypeError('Not a Function');
}
// 保存调用bind的函数
const _this = this;
// 保存参数
const args = Array.prototype.slice.call(arguments, 1);
// 返回一个函数
return function F() {
// 判断是不是new出来的,如果被new创建实例,不会被改变上下文!
if (this instanceof F) {
// 如果是new出来的
// 返回一个空对象,且使创建出来的实例的__proto__指向_this
return new _this(...args, ...arguments);
} else {
// 如果不是new出来的改变this指向,且完成函数柯里化
return _this.apply(context, args.concat(...arguments));
}
};
};
value = 2;
let foo = {
value: 1,
};
let bar = function (name, age) {
console.log(name, age, this.value);
};
let fn = bar.myBind(foo, 'JohnYu', 27);
let a = new fn(); // JohnYu 27 undefined
console.log(a.__proto__); //bar {}
bar.myBind(foo, 'JohnYu', 27)(); // JohnYu 27 1
案例
var num = 1;
let obj = {
num: 2,
add: function() {
this.num = 3;
// 这里的立即指向函数,因为我们没有手动去指定它的this指向,所以都会指向window
(function() {
// 所有这个 this.num 就等于 window.num
console.log(this.num);
this.num = 4;
})();
console.log(this.num);
},
sub: function() {
console.log(this.num)
}
}
// 下面逐行说明打印的内容
/**
* 在通过obj.add 调用add 函数时,函数的this指向的是obj,这时候第一个this.num=3
* 相当于 obj.num = 3 但是里面的立即指向函数this依然是window,
* 所以 立即执行函数里面console.log(this.num)输出1,同时 window.num = 4
*立即执行函数之后,再输出`this.num`,这时候`this`是`obj`,所以输出3
*/
obj.add() // 输出 1 3
// 通过上面`obj.add`的执行,obj.name 已经变成了3
console.log(obj.num) // 输出3
// 这个num是 window.num
console.log(num) // 输出4
// 如果将obj.sub 赋值给一个新的变量,那么这个函数的作用域将会变成新变量的作用域
const sub = obj.sub
// 作用域变成了window window.num 是 4
sub() // 输出4
注意:闭包里面的自执行匿名函数不属于任何对象,他不是一个对象的方法
总结
我们来回顾一下
如何准确判断this指向的是什么?
- 函数是否在new中调用(new绑定),如果是,那么this绑定的是新创建的对象。
- 函数是否通过call,apply,bind调用,如果是,那么this绑定的就是指定的对象。
- 函数是否在某个上下文对象中调用(隐式绑定),如果是的话,this绑定的是那个上下文对象。一般是obj.foo()
- 如果以上都不是,那么使用默认绑定。如果在严格模式下,则绑定到undefined,否则绑定到全局对象。
- 如果把Null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
- 如果是箭头函数,箭头函数的this继承的是外层代码块的this。