你不知道的JS(四):ths机制

前言

  • JavaScript并不具有动态作用域,只有词法作用域。但是this机制某种程度上很像动态作用域。

  • this关键字时JavaScript中最复杂的机制之一。它被自动定义在所有函数的作用域中。

一、为什么使用this

先来看一段代码,常见使用this的写法

function identify() {
	return this.name.toUpperCase();
}
function speak() {
	var greeting = "Hello, I'm " + identify.call(this);
	console.log(greeting);
}
var me = {
	name: "Kyle";
};
var you = {
	name: "Reader";
}
identify.call(me); // KYLE
identify.call(you); // READER
speak.call(me); // Hello, 我是KYLE
speak.call(you); // Hello, 我是READER
  • 这段代码可以在不同的上下文对象(me和you)中重复使用函数identify()speak(),不用针对每个对象编写不同版本的函数。
  • 如果不使用this,那就需要给identify()speak()显式传入一个上下文对象。

不使用this的写法:

function identify(context) {
	return context.name.toUpperCase();
}
function speak(context) {
	var greeting = "Hello, I'm " + identify(context);
	console.log(greeting)
}
identify(you); // READER
speak(me); // hello,I’m KYLE
  • 通过对比可以看出,this提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计得更简洁并且易于复用。
  • 这里就体现this的好处:随着你使用模式越来越复杂,显示传递上下文对象会让代码变得越来越混乱,使用this则不会。

二、调用栈和调用位置

  • this不是指向函数本身,或者函数的词法作用域。
  • this是在运行时绑定的,它的上下文取决于函数调用时的各种条件。
  • 当一个函数被调用时,会创建一个活动记录(也称上下文)。
  • 这个记录会包含函数在哪里被调用(调用栈),函数的调用方式传入的参数等信息。this就是这个记录的一个属性,会在函数执行过程中用到。

下面代码说明什么是调用栈调用位置

function baz() {
	// 当前调用栈是:baz
	// 因此,当前的调用位置是全局作用域
	console.log("baz");
	bar();
}
function bar() {
	// 当前调用栈是baz -> bar
	// 因此,当前调用位置在baz中
	console.log("bar");
	foo(); // foo的调用位置
}
function foo() {
	// 当前的调用栈是baz -> bar -> foo
	// 因此,当前的调用位置在bar中
	console.log("foo");
}
baz(); // baz的调用位置

可以把调用栈想象成一个函数调用链。使用浏览器调试工具可以查看到函数调用列表

三、绑定规则

先找到调用位置,然后判断需要应用下面四条规则的哪一条。

1. 默认绑定

函数直接调用

function foo() {
	console.log(this.a);
}
var a = 2;
foo(); // 2

this.a被解析成全局变量a。严格模式下则不是。

2. 隐式绑定

function foo() {
	console.log(this.a)
}
var obj = {
	a: 2,
	foo: foo
};
obj.foo() // 2
  • 调用位置使用obj来引用函数,或者说作为对象的方法进行调用。
  • 函数调用中的this被绑定到obj上,因此this.a和obj.a是一样的。

隐式丢失

function foo() {
	console.log(this.a)
}
var obj = {
	a: 2,
	foo: foo
};
var bar = obj.foo;
var a = "opps, global";
bar(); // opps, global

这里函数的调用跟obj没有关系了,虽然obj对函数存在引用,但是函数不是作为对象的方法调用。

回调函数丢失this绑定

function foo() {
	console.log(this.a)
}
var obj = {
	a: 2;
	foo: foo
};
var a = "opps, global"
setTimeout(obj.foo, 100); // "opps, global"

这是因为参数传递其实就是一种隐式赋值

上面代码中的定时器:

function setTimeout(fn, delay) { // 这里相当于var fn = obj.foo
	// 等待delay毫秒
	fn(); //调用位置
}
  • this的改变是意想不到的。实际上,你无法控制回调函数的执行方式。
  • 因此就没有办法控制调用位置以得到期望的绑定。之后会通过固定this来修复这个问题。

3. 显式绑定

使用call()、apply()和bind()

function foo() {
	console.log(this.a);
}
var obj = {
	a: 2;
}
foo.call(obj); // 2

通过call可以在调用foo()时,强制把它的this绑定到obj上。

4. 绑定例外

  • 如果把nullundefined作为this的绑定对象传入call,apply或者bind。
  • 这些值在调用时会被忽略,实际应用的时默认绑定规则。
function foo() {
	console.log(this.a);
}
var a = 2;
foo.call(null) // 2
  • 但是这样会有副作用,更安全的时传入空对象Object.create(null),可以避免不必要的this绑定。

5. 箭头函数

箭头函数根据外层(函数或全局)作用域来决定this,且无法修改。

function foo() {
 setTimeout(() => {
 	// 这里的this在词法上继承自foo()
 	console.log(this.a)
 }, 100);
}
var obj = {
	a: 2;
};
foo.call(obj); // 2
  • 实际上,在ES6之前,经常使用一种与箭头函数几乎一样的模式。
  • 就是使用一个变量保存this,并向下传递。
function foo() {
	var self = this;
	setTimeout(() => {
 		console.log(self.a);
 	}, 100);
}
var obj = {
	a: 2;
};
foo.call(obj); // 2

四、总结

1. this机制

this的作用:this提供了一种更优雅的方式来隐式“传递”一个对象引用。(避免传递上下文)

其实this执行主要三种情况,

  • 直接调用函数,this指向window,如果是严格模式指向的是undefined。
  • 作为对象的方法调用,this指向该对象。
  • 箭头函数没有自己this,this指向与上一级作用域中的this一致,且不能修改。

call、apply、bind、new这几个修改this执行都是类似的,使用的是第二种情况,将函数作为传入的对象(new操作时是新建的对象)的方法调用,从而改变this指向。

注意:函数对参数存在隐式赋值,如果将函数作为参数传入,无法确定this指向。

this绑定的优先级

  • 箭头函数 > new > 显式 > 隐式 > 默认绑定

可以查看这几个方法的实现

2. call

 Function.prototype.myCall = function (context,...args) {
     // 判断
    if(typeof this !== 'function'){
      throw new Error(`${this} is not function`)
    }
    if(!context instanceof Object){
      throw new Error(`${context} is not Object`)
    }
    // 关键两步:
    // 将函数绑定到对象上作为对象上的方法,这里的this就是调用call方法的函数
    context.fn = this
    // 作为对象对象中的方法指向
    const result =  context.fn(...args)
    
    //删除被绑定的方法
    delete context['fn']
    return result
 }

3. apply

 Function.prototype.myApply = function (context,args) {
     // 判断
    if(typeof this !== 'function'){
      throw new Error(`${this}is not function` )
    }
    if(!context instanceof Object){
      throw new Error(`${context} is not Object`)
    }
    if(args && !Array.isArray(args)){
      throw new Error(`${args} is not Array`)
    }
    
    // 主要两步:
    //将函数绑定到对象上作为对象上的方法
    context.fn = this
    //执行对象中的方法
    const result = context.fn(...args)
    
    return result
}

可以看到apply()bind()的实现很类似,主要区别在于apply()第二个参数接受数组

4. bind

  Function.prototype.myBind = function (context) {
     // 保存this
    const self = this;
     
     // 判断
    if (typeof self !== "function") {
      throw new Error(`${self}is not function`);
    }
    if (!context instanceof Object) {
      throw new Error(`${context} is not Object`);
    }
     
    //bind方法返回一个函数,这里相当于返回一个bind方法
    return function (...args) {
      //将函数绑定到对象上作为对象上的方法
      context.fn = self
      //执行对象中的方法
      const result = context.fn(...args);
       
      return result;
    };
  };

bind和前面两种方法的区别:bind()返回一个函数,这个函数的主要功能和bind()一样。

5. new

  • new操作过程中先新建一个对象,然后将构造函数作为对象的方法调用
  • 修改this指向的方式和前面几种方法类似,只是多了其他内容。

后续在原型对象中进行分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值