03this指向问题

133 阅读12分钟

为什么需要this?

在常见的编程语言中,几乎都有this这个关键字(Objective-C中使用的是self),但是JavaScript中的this和常见的面向对象语 言中的this不太一样:

  1. 常见面向对象的编程语言中,比如Java、C++、Swift、Dart等等一系列语言中,this通常只会出现在类的方法中。
  2. 也就是你需要有一个类,类中的方法(特别是实例方法)中,this代表的是当前调用对象。
  3. 也就是你需要有一个类,类中的方法(特别是实例方法)中,this代表的是当前调用对象。
  4. 但是JavaScript中的this更加灵活,无论是它出现的位置还是它代表的含义。

演示

image-20220803145607964

其实我们能够发现,上面如果不使用this也是可以的,但是如果对象的名字发生改变呢?那么我们是不是要修改很多的东西,所以说如果使用this会让很多东西变得更加的方便。

this的指向

//在全局作用域下 浏览器:window  Node:{} 空的对象 或者说成global
console.log(this);

函数的this

function foo(){
  console.log(this)
}

// 以函数方式调用
foo() // window

// 以对象属性方式调用
var person = {
  name:'cs',
  foo:foo
}
person.foo() // person

// call apply bind
foo.call('abc') // abc

当我们以不同的方式去调用的时候,this所指向的就是不同的值,它和函数所处的位置是没有关系的。

this到底指向什么?

从上面简短的代码中,我们能够看到,我们采用三种不同的方式对函数进行调用,它产生了三种不同的结果。

启示

  1. 函数在调用时,JavaScript会默认给this绑定一个值;
  2. this的绑定和定义的位置(编写的位置)没有关系;
  3. this的绑定和调用方式以及调用的位置有关系;
  4. this是在运行时被绑定的;

绑定规则

  1. 绑定一:默认绑定;
  2. 绑定二:隐式绑定;
  3. 绑定三:显示绑定;
  4. 绑定四:new绑定;

规则一:默认绑定

什么情况下使用默认绑定呢?

独立函数调用。独立的函数调用我们可以理解成函数没有被绑定到某个对象上进行调用;

案例一

// 独立函数调用
function foo() {
  console.log('foo=>',this)//window
}
foo()

毫无疑问,这里的this所指向的就是window

案例二

function foo1() {
  console.log(this)
}
function foo2() {
  foo1()
  console.log(this)
}
function foo3() {
  foo2()
  console.log(this)
}
foo3()

同样的这里的每一个this,所指向的都是window,因为他们都属于函数的独立调用。

案例三

var obj = {
  name:'cs',
  foo4:function () {
    console.log('案例三',this); //window
  }
}
var bar = obj.foo4;
bar()

在这里依旧是函数的独立调用,this所指向的依旧是window。需要注意的是,要判断函数的调用的时候有没有主题,而不是在定义的时候。

和定义的位置是没有关系的,即使你是放在了对象里面。

案例四

function foo() {
  function bar() {
    console.log(this);
  }
  return bar
}
var fn = foo();
fn()

这同样也是函数的独立调用,所以this指向的就是window

注意点

use strict(严格模式)模式下,独立调用的函数中的this指向的是undefined

规则二:隐式绑定

另外一种比较常见的调用方式是通过某个对象进行调用的:也就是它的调用位置中,是通过某个对象发起的函数调用。

案例一

function foo() {
  console.log(this); // obj
}
var obj = {
  name:'cs',
  foo:foo
}
obj.foo()

obj.xx方式进行调用的时候,就会obj绑定到执行上下文中的this

案例二

var obj = {
  name:'cs',
  eating:function () {
    console.log(this.name);
    console.log(obj.name);
  }
}
obj.eating() // 以obj进行调起的

规则三:显示绑定

隐式绑定有一个前提条件:

  1. 必须在调用的对象内部有一个对函数的引用(比如一个属性);
  2. 如果没有这样的引用,在进行调用时,会报找不到该函数的错误;
  3. 正是通过这个引用,间接的将this绑定到了这个对象上;

如果我们不希望在 对象内部 包含这个函数的引用,同时又希望在这个对象上进行强制调用,该怎么做呢?

  1. JavaScript所有的函数都可以使用call和apply方法(这个和Function的Prototype有关)。
    • 其实非常简单,第一个参数是相同的,后面的参数,apply为数组,call为参数列表;
  2. 这两个函数的第一个参数都要求是一个对象,这个对象的作用是什么呢?就是给this准备的。
  3. 在调用这个函数时,会将this绑定到这个传入的对象上。

因为上面的过程,我们明确的绑定了this指向的对象,所以称之为 显示绑定。

案例一

function foo() {
  console.log(this);
}
foo.call();
foo.apply()

callapply没有指定对象的时候,this依旧是指向window的。

function foo() {
  console.log(this);
}

var obj = {
  name:"cs",
}
foo.call(obj);//obj
foo.apply()//window
foo.call(undefined/null);//window
foo.bind(obj)() // obj

改变当前的this指向,但是下面对于当前函数进行调用的时候,不会进行改变this

call和apply和bind的区别

function sum(n1,n2){
	return n1+n2
}
// 区别不是很大,仅仅是在参数传递的时候。
sum.call(obj,20,30)
sum.apply(obj,[20,30])
sum.bind(obj,20,30)() //bind返回的是一个函数,需要进行一次调用

默认绑定和bind冲突

function foo() {
  console.log(this);
}

var obj = {
  name:"cs",
}

//这里就属于bind和默认绑定冲突的问题,显示绑定优先级大于默认绑定
var newFoo = foo.bind(obj);
newFoo() //obj

规则四:new绑定

JavaScript中的函数可以当做一个类的构造函数来使用,也就是使用new关键字。

使用new关键字来调用函数是,会执行如下的操作:

  1. 创建一个全新的obj对象;
  2. 这个新对象会被执行prototype连接;
  3. 这个新对象会绑定到函数调用的this上(this的绑定在这个步骤完成);
  4. 如果函数没有返回其他对象,表达式会返回这个新对象;

new绑定

function Person(name,age) {
  this.name = name;
  this.age = age;
}

var p1 = new Person('cs1',10);
console.log(p1.name,p1.age);

var p2 = new Person('cs2',20);
console.log(p2.name,p2.age);

通过一个new关键字调用一个函数时(构造器),这个时候this是在调用这个构造器时创建出来的对象。

this = 创建出来的对象。

这个绑定的过程就是this绑定。

内置函数的this

setTimeout

// setTimeout()接收两个参数,一个是函数,一个是时间
function csSetTimeout(fn,TimeOut) {
  fn()
}
csSetTimeout(function () {
  
},3000)

// setTimeout 第一个参数:函数的this,和内部到底是如何调用的是有关系。
setTimeout(function () {
  console.log(this);// window
},1000)
setTimeout(() => {
  console.log(this);// window
}, 2000);

所以能够发现,setTimeout对于传入的函数是独立调用的,因为this指向的都是window。

div的点击

const boxDiv = document.getElementsByClassName('box');

console.log(boxDiv[0]);
boxDiv[0].addEventListener('click', ()=> {
  console.log(this);//window
})
boxDiv[0].addEventListener('click', function(){
  console.log(this);//box
})


boxDiv[0].onclick = function () {
  console.log(this);//box
  // boxDiv[0].onclick 进行了隐式的绑定
}
boxDiv[0].onclick =  () => {
  console.log(this);// 空
}

所以当this为box的时候,我们就能够对box进行一些操作,比如说添加属性等等一系列操作DOM的事件。

数组的forEach

var names = ['小白','小汪','小李'];
names.forEach(function () {
  console.log(this); // window
})

names.forEach( ()=> {
  console.log(this); // window
})

// 改变this
names.forEach( function(){
  console.log(this); // obj
},'obj')
names.forEach( ()=> {
  console.log(this); // window,箭头函数改变不了
},'obj')

this绑定优先级

  1. 默认规则的优先级最低。
  2. 显示绑定优先级高于隐式绑定。
  3. new绑定优先级高于隐式绑定。
  4. new绑定优先级高于bind
    • new绑定和call、apply是不允许同时使用的,所以不存在谁的优先级更高。
    • new绑定可以和bind一起使用,new绑定优先级更高。

显示大于隐式

function foo() {
  console.log(this);//more
}
var obj = {
  name:'obj',
  foo:foo;
}
var more = {
  name:'more';
}
obj.foo.call(more);

显示绑定的this要大于隐式绑定的this

new大于隐式

var obj = {
  name:'cs',
  foo:function() {
    console.log(this);
  }
}
var p = new obj.foo()//foo()

如果this是obj那就说明,隐式绑定的优先级大,如果this是foo这个函数,那就说明 new的优先级更高。

new大于显示

function Foo() {
  console.log(this);
}
var f =  Foo.bind('abc');
var o = new f();

new 不能和 call 和 apply 不能一起使用,new是之间去调用一个函数,apply 和 call 也是,bind则不是。

bind大于apply/call

function foo() {
  console.log(this); //aaa
}
var bindFn = foo.bind("aaa");
bindFn.call("bbb");

总结

new绑定 > 显示绑定(apply/call/bind) > 隐式绑定(obj.xxx) > 默认绑定

this规则之外

忽略显示绑定

如果在显示绑定中,我们传入一个null或者undefined,那么这个显示绑定会被忽略,使用默认规则。

function foo() {
  console.log(this);
}
// 下面都是window
foo.call(null);
foo.apply(null);
foo.bind(null)();
foo.call(undefined);
foo.apply(undefined);
foo.bind(undefined)();

间接函数引用

var obj1 = {
  name:'cs1',
  foo:function() {
    console.log(this);
  }
};

var obj2 = {
  name:'cs2',
};
//这样写的时候,上面必须加分号
(obj2.bar = obj1.foo)();//window,相当于(obj2.bar = obj1.foo)是foo函数,所以独立调用

这个时候的this相当于是函数的单独调用,所以是window

箭头函数的this

箭头函数:arrow function

箭头函数不使用this的四种标准规则(也就是不绑定this),而是根据外层作用域来决定this,this是不会改变的。

const app = () => {
  console.log(this);
}
app() // window

使用场景

var obj = {
  data:[],
  getData(){
    //模拟发送网络请求
    var _this =  this
    setTimeout( function(){
      var res = ['你好'];//模拟拿到数据
      _this.data = res;
    }, 2000);
  }
}
obj.getData() // 隐式绑定,所以getData中的this就是obj

我们能够从上面的代码中发现,setTimeout如果写成普通函数形式,那么它的this将会是window,所以如果使用this.data = res,那么是没有效果的,因为window上面是没有data的,所以我们用了_this = this,来代替所谓的this

但是使用箭头函数就非常简单了,因为它里面的this是上层作用域的this,那么毫无疑问就是obj了。

this面试题

例题一

var name = "window";

var person = {
  name: "person",
  sayName: function () {
    console.log(this.name);
  }
};

function sayName() {
  var sss = person.sayName;
  sss(); // window: 独立函数调用
  person.sayName(); // person: 隐式调用
  (person.sayName)(); // person: 隐式调用
  (b = person.sayName)(); // window: 赋值表达式(独立函数调用)
}

sayName();

例题二

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(隐式绑定)
// person1.foo1.call(person2); // person2(显示绑定优先级大于隐式绑定)

// person1.foo2(); // window(不绑定作用域,上层作用域是全局)
// person1.foo2.call(person2); // window

// person1.foo3()(); // window(独立函数调用)
// person1.foo3.call(person2)(); // window(独立函数调用)
// person1.foo3().call(person2); // person2(最终调用返回函数式, 使用的是显示绑定)

// person1.foo4()(); // person1(箭头函数不绑定this, 上层作用域this是person1)
// person1.foo4.call(person2)(); // person2(上层作用域被显示的绑定了一个person2)
// person1.foo4().call(person2); // person1(上层找到person1)

例题三

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
person1.foo1.call(person2)      // person2(显示高于隐式绑定)

person1.foo2()              		// person1 (上层作用域中的this是person1)
person1.foo2.call(person2) 			// person1 (上层作用域中的this是person1)

person1.foo3()() 								// window(独立函数调用)
person1.foo3.call(person2)() 		// window
person1.foo3().call(person2) 		// person2

person1.foo4()() 								// person1
person1.foo4.call(person2)() 		// person2
person1.foo4().call(person2) // person1

例题四

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()() // window
person1.obj.foo1.call(person2)() // window
person1.obj.foo1().call(person2) // person2

person1.obj.foo2()() // obj
person1.obj.foo2.call(person2)() // person2
person1.obj.foo2().call(person2) // obj

// 上层作用域的理解
// var obj = {
//   name: "obj",
//   foo: function() {
//     // 上层作用域是全局
//   }
// }

// function Student() {
//   this.foo = function() {

//   }
// }

call函数的实现

// 给所有的函数添加一个 zfcall 方法 ...args ES6剩余参数
Function.prototype.zfcall = function(thisBinds,...args) {
  // 在这里去执行,调用 zfcall 的函数。

  // 1 首先找要到谁调用了改函数。
  var fn = this;// foo.zfcall 相当于是隐式调用,那么this就是调用的函数

  // 2 对thisBinds 转化为对象,防止传入的是非对象类型(字符串,数字)。
  // 但是还有一种特殊情况,那就是 null 和 undefined ,这个时候this就是window
  thisBinds = thisBinds ?Object(thisBinds) :window;

  // 3 调用被执行的函数
  thisBinds.fn = fn ; //隐式绑定,将传入的参数,改变为原来函数的this
  var res = thisBinds.fn(...args);//扩展运算符
  delete thisBinds.fn;
  return res;
}

function foo() {
  console.log('foo函数被执行',this);
}

function sum(s1,s2){
  console.log('sum函数被执行',this);
  return s1 + s2;
}

// 系统调用
// foo.call()

// 自定义调用
foo.zfcall({name:'cs'});
const a = sum.zfcall('str',20,30);

console.log(a); // 50

认识arguments

arguments:是一个对应于 传递给函数的参数> 的类数组对象。

function foo(x1,x2,x3) {
  console.log(arguments.length); // 5
}
foo(1,2,3,4,4);

这里的arguments的长度和内容,是你给函数所传递的实参,而不是在函数声明的形参。

具体来说:我们给函数所传递过去的参数,都会被放到一个类数组对象里面,而这个对象就是arguments

对于arguments常见的操作一共有三个:获取参数的长度,根据索引获取某个参数,获取arguments所在的函数。

对argument的操作

function foo(x1,x2,x3) {
  console.log(arguments.length); // 5
  console.log(arguments[1]);
  console.log(arguments.callee);// 会返回函数的所有信息
}
foo(1,2,3,4,4);

类数组和数组

类数组意味着它不是一个数组类型,而是一个对象类型:

  1. 它却拥有数组的一些特性,比如说length,比如可以通过index索引来访问;
  2. 但是它却没有数组的一些方法,比如forEach、map等;

类数组转成数组

这里虽然使用的是arguments,但是适用于任何的类数组。

扩展运算符

function foo(x1,x2,x3) {
  // 1 扩展运算符
  const arr = [...arguments]
  console.log(arr);// 此时的arr就具有了数组的所有方法
}
foo(1,2,3,4,4)

for循环完成

function foo(x1,x2,x3) {
  var arr = [];
  for(var i=0; i<arguments.length;i++){
    arr.push(arguments[i])
  }
  console.log(arr);
}
foo(1,2,3,4,4)

Array.form

function foo(x1,x2,x3) {
  var arr = Array.from(arguments)
  console.log(arr);
}
foo(1,2,3,4,4)

Array.prototype

function foo(x1,x2,x3) {
  var arr = Array.prototype.slice.call(arguments);
  console.log(arr);
}
foo(1,2,3,4,4)

// 详细解释
Array.prototype.zfslice = function(start,end){
  start = start || 0;
  end = end || this.length
  // 当使用 call 去调用的时候,this所指向的就是 arguments了
  var arr = this; // 伪数组
  var newArray = [];
  for(var i = start; i < end; i++){
    newArray.push(arr[i])
  }
  return newArray;
}
//arr 就是返回的新数组了,后面还可以传入截取的位置。
var arr = Array.prototype.zfslice.call(arguments,1,2); 

Array.prototype.slice主要是为了拿到slice方法,数组是直接可以调用这个方法的,这里是个伪数组,所以要通过Array去拿取。