对JavaScript中this指向的总结
前言
在常见的面向对象的编程语言如java、C#等中,this关键字通常只会出现在类的方法中,尤其是在类的实例对象中,this代表的是当前的实例对象
例如如下的C#代码:
//定义了一个名为StudyInfo的类
public class StudyInfo
{
private string Name { get; set; }
private int Count { get; set; }
/// <summary>
/// 构造函数
/// </summary>
public StudyInfo(string myName,int myCount)
{
this.Name = myName;
this.Count = myCount;
}
public void getDetailMassage()
{
Console.WriteLine(this.Name);
Console.WriteLine(this.Count);
}
}
/// <summary>
/// 构建 类StudyInfo的第一个实例对象info1
/// </summary>
StudyInfo info1 = new StudyInfo("张三", 888);
/// <summary>
/// 构建 类StudyInfo的第一个实例对象info2
/// </summary>
StudyInfo info2 = new StudyInfo("李四", 999);
info1.getDetailMassage();
info2.getDetailMassage();
但是JavaScript中的this更加的灵活,无论是它出现的位置还是它代表的函数,所以我们需要好好的了解一下它。
javascript是一门什么语言?
也许当你看到在JavaScript中也是使用的this关键字,可能就会“顺其自然”的认为它是一门面向对象的语言。
但面向对象需要包含三大特性,封装、继承、多态,而javascript中只有封装。继承也是模拟继承,所以javascript是一门基于对象的脚本语言,而非面向对象
并且,从语言的分类上来说,Javascript是一门解释性语言,它是单线程的,同一时间只会做同一件事,会将每一个任务都分割成很多个片段,随机执行这些片段
语言的大致分类
-
编译性语言:C、C++
- 它是通篇翻译后再执行
- 优点:快
- 缺点:不好移植
-
解释性语言:javascript、PHP
- 它是一行一行执行的,单线程
- 优点:好移植,直接能翻译成计算机可以看得懂的东西
- 缺点:稍慢
-
oak语言:java
- .java------>jvm虚拟机(javac指令)----->.class----->(java指令)执行代码
this的绑定规则
Q:在正式了解JavaScript中this的指向规则前,我们先来思考一个问题,为什么要使用this,它有什么好处呢?
A:在某些函数或者方法的编写中,this可以让我们更加便捷的方式来引用对象,在进行一些API设计时,代码更加的简洁和易于复用
规则1:默认绑定
使用场景:独立函数调用(也就是当函数没有被绑定到某个对象上进行调用)时,this的绑定规则是默认绑定。通常默认绑定时,函数中的this指向全局对象(window)
//普通函数调用
function exampleFun1(){
console.log(this); //window
}
exampleFun1(); //在函数调用的位置,是独立调用的,并没有绑定任何对象,this默认绑定
//函数调用链
function exampleFunPart1(){
exampleFunPart3(); //window
//在函数实际调用的位置,其实还是独立调用的,并没有绑定任何对象,this默认绑定
}
function exampleFunPart2(){
console.log(this);
}
function exampleFunPart3(){
exampleFunPart2();
}
exampleFunPart1();
//将函数作为参数,传递到函数内部
function exampleFun2(func){
func();
}
function exampleFun2_demo(){
console.log(this);
}
exampleFun2(exampleFun2_demo); //window
//也是一样的,在函数实际调用的位置,其实还是独立调用的,并没有绑定任何对象,this默认绑定
规则2:隐式绑定
在开发中,另外一种比较常见的函数调用方式是通过某个对象来进行调用的,也就是在函数的实际调用位置中,是通过某个对象发起的函数调用。
//例子1:通过对象来调用函数
function fun1(){
console.log(this);
}
const testObj = {
name:"happy",
fun1
}
//通过对象来调用函数,此时 函数的this会隐式的绑定到对象上
testObj.fun1(); //{name: 'happy', fun1: ƒ}
//例子2:通过对象的对象来调用函数
function fun2(){
console.log(this);
}
const testObj2 = {
name:"testObj2",
fun2
}
const testObj1 = {
name:"testObj1",
testObj2
}
//通过对象来调用函数,此时 函数的this会隐式的绑定到【调用它的对象】上
testObj1.testObj2.fun2(); //{name: 'testObj2', fun2: ƒ}
//例子3
function fun3(){
console.log(this);
}
const testObj3 = {
name:"happy",
fun3
}
const getFun = testObj3.fun3;
//此时调用函数会隐式的绑定this到testObj3上吗? no,在实际函数调用的位置,是独立调用的,this默认绑定
getFun(); //window
//相当于就是把这个函数拿出来了,然后再调用
规则3:显示绑定
隐式绑定有一个前提条件:
- 必须在调用的
对象内部
有一个对函数的引用(比如一个属性); - 如果没有这样的引用,在进行调用时,会报找不到该函数的错误;
- 正是通过这个引用,间接的将this绑定到了这个对象上;
如果我们不希望在 对象内部 包含这个函数的引用,同时又希望在这个对象上进行强制调用,该怎么做呢?
-
JavaScript所有的函数都可以使用call、apply、bind方法(这个和Prototype有关)。
-
对于call和apply:
-
它俩的区别其实非常简单,第一个参数是相同的,后面的参数,apply为数组,call为参数列表;
-
这两个函数的第一个参数都要求是一个对象,这个对象的作用是什么呢?就是给this准备的。
-
在调用这个函数时,会将this绑定到这个传入的对象上。
-
因为上面的过程,我们明确的绑定了this指向的对象,所以称之为 显示绑定。
function myDemoFun(number1,number2){
console.log(this.name);
console.log(number1 + number2);
}
const myObj1 = {
name:"我是obj1"
}
const myObj2 = {
name:"我是obj2"
}
const myObj3 = {
name:"我是obj3"
}
//使用call 来显示绑定
myDemoFun.call(myObj1,66,88); //我是obj1 154
//使用apply 来显示绑定
myDemoFun.apply(myObj2,[33,22]); //我是obj2 55
//使用bind 来拿到显示绑定后的函数 之后就可以重复使用绑定后的函数
const afterBindFun = myDemoFun.bind(myObj3);
afterBindFun(11,22);
afterBindFun(4,22);
afterBindFun(2,22);
afterBindFun(88,22);
看几个例子
那么在了解了上述几种this规则后,我们来看看几种特殊的this,他们是使用以上的哪种规则
(1)JavaScript的内置函数 setTimeout/setInterval
有些时候,我们会调用一些JavaScript的内置函数,或者一些第三方库中的内置函数。这些内置函数会要求我们传入另外一个函数,我们自己并不会显示的调用这些函数,而是在JavaScript内部或者第三方库内部会帮助我们执行;
- setTimeout或者 setInterval中会传入一个函数,这个函数中的this通常是window
//part1-传入的是个箭头函数
setTimeout(() => {
console.log("我是一个定时器");
console.log(this); //window
},2000);
//part1-传入的是个函数声明
setTimeout(function(){
console.log("我是一个定时器");
console.log(this); //window
},2000);
//part2
let counter = 0;
let myTimer = setInterval(() => {
console.log("我是一个重复计数器");
console.log(this); //window
counter++;
if(counter >= 10){
console.log("计数结束");
clearInterval(myTimer);
}
}, 1000);
至于为啥this的指向是window,我们可以从mdn文档中找到答案:
https://developer.mozilla.org/zh-CN/docs/Web/API/setTimeout
简单来说,就是我们传递进去的函数是独立调用的,他采用的是上述的第一种【默认绑定】
(2)高阶函数forEach
[1,2,3,4,5].forEach(function(item){
console.log(this); //window
console.log(item);
});
也是一样的,在默认情况下,我们传递进去函数调用是独立调用的,他采用的是上述的第一种【默认绑定】
但是,forEach函数可以传递第二个参数来指定this的指向
let obj = {
name:"objPart"
};
[1,2,3,4,5].forEach(function(item){
console.log(this); //obj
console.log(item);
},obj);
不过,如果你传递进去的第一个函数是箭头函数,那么this的指向还是window,因为箭头函数没有自己的this,即使是显示绑定也无效,它会从作用域链上,向上寻找父级的this,后文会来继续补充说明箭头函数
let obj = {
name:"objPart"
};
[1,2,3,4,5].forEach(el => {
console.log(this); //window
console.log(el);
},obj);
(3) 元素的点击
当有一个div,然后我们去监听它的点击事件,其函数中的this的指向是指向谁?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>关于this的指向的demo</title>
</head>
<body>
<div class="box" style="width: 100px;height: 100px;background-color: pink;">box</div>
<script>
//获取box元素的实例对象
let boxElement = document.getElementsByClassName("box")[0];
//回调函数是个函数声明
boxElement.onclick = function(){
console.log("点击了box");
console.log(this); //box元素
}
//回调函数是个表达式
boxElement.onclick = () =>{
console.log("点击了box by arrow function");
console.log(this); //window
}
</script>
</body>
</html>
很明显,boxElement.onclick 函数的调用是由box对象来调用的,采用的是【隐式绑定】,但如果给的是个箭头函数,this的指向是window,和上述同理
规则4:new关键字
JavaScript中的函数可以当做一个类的构造函数来使用,也就是使用new关键字
当执行new命令时,其后面的函数依次执行以下步骤:
- (1)创建一个空对象,作为要返回的实例对象
- let obj = {};
- (2)将这个空对象的原型,指向构造函数的prototype属性
- obj.proto_ = Fun.prototype
- (3)将这个空对象赋值给函数内部的this关键字
- this = obj
- (4)开始执行构造函数内部的代码
- (5) 最后返回时,如果构造函数有return的对象,那么就会返回该对象,否则就会隐式返回原本要返回的实例对象
构造函数之所以叫构造函数,就是操作一个空对象,将其构造成需要的样子
function MyPerson1(){
this.myName = "person1";
this.getNumber = function(){
return 999;
}
console.log(this); //MyPerson1
}
let person1 = new MyPerson1();
绑定规则的优先级
结论是: new关键字 > 显示绑定 > 隐式绑定 > 默认绑定
默认绑定的优先级最低
默认规则的优先级是最低的,因为当存在其他规则时,就会通过其他规则的方式来绑定this
显示绑定优先级大于隐式绑定
let myTestObj1 = {
name: "testObj1",
getName:function(){
console.log(this.name);
}
}
let myTestObj2 = {
name: "testObj2",
getName:function(){
console.log(this.name);
}
}
//隐式绑定和显示绑定同时存在
myTestObj1.getName.call(myTestObj2); //testObj2
new关键字的优先级 大于 隐式绑定
function Foo(){
console.log(this);
}
let testObj = {
Foo
}
new testObj.Foo(); //Foo(){}
new关键字的优先级大于 显示绑定
new关键字是不允许和call apply一起用
//new关键字是不允许和call apply一起用,是会报错的
function Foo(){
console.log(this);
}
let testObj = {
name:"testObj"
}
let foo = new Foo.call(testObj); //报错:TypeError: Foo.call is not a constructor
//ok 很明显在这里语法分析就已经报错了
//不过 如果你让 Foo.call() 作为一个整体,先执行,那么就是可以的
function Foo(){
console.log(this);
return function FooSon(){
console.log(this);
}
}
let testObj = {
name:"testObj"
}
let foo = new (Foo.call(testObj)); //FooSon
new 关键字和bind
function Foo(){
console.log(this);
}
let testObj = {
name:"testObj"
}
let foo = new (Foo.bind(testObj)); //Foo
箭头函数
正常来说,在我们的日常情况下,就是应用上述的四种规则,但存在一个例外,它就是 ES6中新增的箭头函数
- 箭头函数不会创建自己的this,它只会从自己的作用域链上找父级执行上下文的this
- 箭头函数没有prototype,没有arguments,也不能当构造函数
其他关于箭头函数的细节,可以自行查找资料,这里就不赘述了
最后想要补充和本篇内容相关的两个面试考点
new 和 object.create的区别
首先来简单了解一下 原型和原型链
原型和原型链
最初,由于同一个构造函数的多个实例之间无法共享属性,从而造成对系统资源的浪费的现象存在,所以设计出了javascript原型对象—>prototype
-
所有引用类型(函数,数组,对象)都拥有__proto__属性
-
所有函数都拥有prototype属性(显示原型)
-
原型对象:拥有prototype属性的对象,在定义函数时就会被创建
-
实例对象._ proto_ === 构造函数.prototype
-
原型链
javascript规定,所有对象的原型都有自己的原型对象,并且原型对象也是对象,也会有自己的原型,因此就会形成原型链,原型链的尽头是null
—> Object.prototype._ proto_ === null
根据上述所说的来理解这个图
new和object.create的区别
**object.create()的实现方式**
Object.create = function(Fun){
const F = function(){}
F.prototype = Fun;
return new F();
}
//所以说通过这种方式得到的实例对象.__proto__ === Fun
比较 | new | Object.create |
---|---|---|
构造函数 | 保留原构造函数的属性 | 丢失原构造函数的属性 |
原型链 | 原构造函数的prototype属性 | 就是原构造函数 |
作用对象 | function | function和object |
关于Object.create更详细的了解可以查看MDN文档:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create
手写 call apply bind
//手写call
Function.prototype.myCall = function(thisArg,...args){
//1.获取需要执行的函数
let fn = this;
//2.将thisArg转为对象类型,做一层防呆
thisArg = (thisArg != null && thisArg != undefined) ? Object(thisArg) : window;
//3.将需要调用的函数作为对象的属性绑定到thisArg对象上,但由于thisArg本身就有fu属性,为了避免冲突,在这里使用symbol
const fn_Symbol = Symbol("fn");
//由于这里func是Symbol变量不再是字符串,所以要用中括号获取属性
thisArg[fn_Symbol] = this;
//4.调用函数,拿到结果
let res = thisArg[fn_Symbol](...args);
//5.删除添加到对象thisArg上的这个函数
delete thisArg[fn_Symbol];
//返回调用的结果
return res;
}
//手写apply 与call相比,只有传参的形式不同
Function.prototype.myApply = function(thisArg,args){
//1.获取需要执行的函数
let fn = this;
//2.将thisArg转为对象类型,做一层防呆
thisArg = (thisArg != null && thisArg != undefined) ? Object(thisArg) : window;
//3.将需要调用的函数作为对象的属性绑定到thisArg对象上,但由于thisArg本身就有fu属性,为了避免冲突,在这里使用symbol
const fn_Symbol = Symbol("fn");
//由于这里func是Symbol变量不再是字符串,所以要用中括号获取属性
thisArg[fn_Symbol] = this;
//4.对用户传递进来的参数做一个防呆
args = args || [];
//4.调用函数,拿到结果
let res = thisArg[fn_Symbol](...args);
//5.删除添加到对象thisArg上的这个函数
delete thisArg[fn_Symbol];
//返回调用的结果
return res;
}
//手写bind
Function.prototype.myBind = function(thisArg,...argArray){
//1.获取需要执行的函数
let fn = this;
//2.将thisArg转为对象类型,做一层防呆
thisArg = (thisArg != null && thisArg != undefined) ? Object(thisArg) : window;
//将需要调用的内容放到一个函数中 形成了闭包
function proxyFun(){
//3.将需要调用的函数作为对象的属性绑定到thisArg对象上,但由于thisArg本身就有fu属性,为了避免冲突,在这里使用symbol
const fn_Symbol = Symbol("fn");
//由于这里func是Symbol变量不再是字符串,所以要用中括号获取属性
thisArg[fn_Symbol] = this;
//4.合并参数列表
let allArgs = [...argArray,...args];
//5.调用函数 拿到结果
var result = thisArg[fn_Symbol](allArgs);
//6.删除添加到对象thisArg上的这个函数
delete thisArg[fn_Symbol];
//7. 返回结果
return result;
}
return result;
}
做几个练习题
答案放最后面
练习题1
var name = "window";
var person = {
name: "person",
sayName: function () {
console.log(this.name);
}
};
function sayName() {
var sss = person.sayName;
sss();
person.sayName();
(person.sayName)();
(b = person.sayName)();
}
sayName();
练习题2
var name = 'window'
var person1 = {
name: 'person1',
foo1: function () {
console.log(this.name)
},
foo2: () => console.log(this.name),
foo3: function () {
return function () {
console.log(this.name)
}
},
foo4: function () {
return () => {
console.log(this.name)
}
}
}
var person2 = { name: 'person2' }
person1.foo1();
person1.foo1.call(person2);
person1.foo2();
person1.foo2.call(person2);
person1.foo3()();
person1.foo3.call(person2)();
person1.foo3().call(person2);
person1.foo4()();
person1.foo4.call(person2)();
person1.foo4().call(person2);
练习题3
var name = 'window'
function Person (name) {
this.name = name
this.foo1 = function () {
console.log(this.name)
},
this.foo2 = () => console.log(this.name),
this.foo3 = function () {
return function () {
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.foo1.call(person2)
person1.foo2()
person1.foo2.call(person2)
person1.foo3()()
person1.foo3.call(person2)()
person1.foo3().call(person2)
person1.foo4()()
person1.foo4.call(person2)()
person1.foo4().call(person2)
练习题4
var name = 'window'
function Person (name) {
this.name = name
this.obj = {
name: 'obj',
foo1: function () {
return function () {
console.log(this.name)
}
},
foo2: function () {
return () => {
console.log(this.name)
}
}
}
}
var person1 = new Person('person1')
var person2 = new Person('person2')
person1.obj.foo1()()
person1.obj.foo1.call(person2)()
person1.obj.foo1().call(person2)
person1.obj.foo2()()
person1.obj.foo2.call(person2)()
person1.obj.foo2().call(person2)
练习题1解析
function sayName() {
var sss = person.sayName;
sss(); //【独立调用】 window
person.sayName(); //【隐式绑定】 person
(person.sayName)(); //【隐式绑定】person
(b = person.sayName)(); //【独立调用】window
}
练习题2 解析
// 隐式绑定,肯定是person1
person1.foo1(); // person1
// 隐式绑定和显示绑定的结合,显示绑定生效,所以是person2
person1.foo1.call(person2); // person2
// foo2()是一个箭头函数,不适用所有的规则
person1.foo2() // window
// foo2依然是箭头函数,不适用于显示绑定的规则
person1.foo2.call(person2) // window
// 获取到foo3,但是调用位置是全局作用于下,所以是默认绑定window
person1.foo3()() // window
// foo3显示绑定到person2中
// 但是拿到的返回函数依然是在全局下调用,所以依然是window
person1.foo3.call(person2)() // window
// 拿到foo3返回的函数,通过显示绑定到person2中,所以是person2
person1.foo3().call(person2) // person2
// foo4()的函数返回的是一个箭头函数
// 箭头函数的执行找上层作用域,是person1
person1.foo4()() // person1
// foo4()显示绑定到person2中,并且返回一个箭头函数
// 箭头函数找上层作用域,是person2
person1.foo4.call(person2)() // person2
// foo4返回的是箭头函数,箭头函数只看上层作用域
person1.foo4().call(person2) // person1
练习题3解析
// 隐式绑定
person1.foo1() // peron1
// 显示绑定优先级大于隐式绑定
person1.foo1.call(person2) // person2
// foo是一个箭头函数,会找上层作用域中的this,那么就是person1
person1.foo2() // person1
// foo是一个箭头函数,使用call调用不会影响this的绑定,和上面一样向上层查找
person1.foo2.call(person2) // person1
// 调用位置是全局直接调用,所以依然是window(默认绑定)
person1.foo3()() // window
// 最终还是拿到了foo3返回的函数,在全局直接调用(默认绑定)
person1.foo3.call(person2)() // window
// 拿到foo3返回的函数后,通过call绑定到person2中进行了调用
person1.foo3().call(person2) // person2
// foo4返回了箭头函数,和自身绑定没有关系,上层找到person1
person1.foo4()() // person1
// foo4调用时绑定了person2,返回的函数是箭头函数,调用时,找到了上层绑定的person2
person1.foo4.call(person2)() // person2
// foo4调用返回的箭头函数,和call调用没有关系,找到上层的person1
person1.foo4().call(person2) // person1
练习题4 解析
// obj.foo1()返回一个函数
// 这个函数在全局作用于下直接执行(默认绑定)
person1.obj.foo1()() // window
// 最终还是拿到一个返回的函数(虽然多了一步call的绑定)
// 这个函数在全局作用于下直接执行(默认绑定)
person1.obj.foo1.call(person2)() // window
person1.obj.foo1().call(person2) // person2
// 拿到foo2()的返回值,是一个箭头函数
// 箭头函数在执行时找上层作用域下的this,就是obj
person1.obj.foo2()() // obj
// foo2()的返回值,依然是箭头函数,但是在执行foo2时绑定了person2
// 箭头函数在执行时找上层作用域下的this,找到的是person2
person1.obj.foo2.call(person2)() // person2
// foo2()的返回值,依然是箭头函数
// 箭头函数通过call调用是不会绑定this,所以找上层作用域下的this是obj
person1.obj.foo2().call(person2) // obj
相关参考资料
(1)https://mp.weixin.qq.com/s/hYm0JgBI25grNG_2sCRlTA
Over~ see you next time!