this

92 阅读8分钟

执行上下文

概念

在js中,执行上下文是代码运行的环境。代码只要执行,就有其执行上下文。执行上下文中有:

  • 变量环境:编译阶段,var变量提升,以及执行阶段var变量存储和赋值。
  • 词法环境:编译阶段,let变量放在词法环境,代码执行到块级作用域下,形成词法环境栈,对该块级下let变量存储、赋值。执行完,词法环境中,该块级词法栈出栈。
  • this:this与执行上下文绑定,每个执行上下文都有this.
  • 外部环境

image.png

分类

执行上下文分为:全局&函数执行上下文。

  • 全局执行上下文:任何不在函数内部执行的代码,在执行中就会创建全局执行上下文,这类代码在执行时,会首先创建window对象,然后将this指向window,一个程序中只有一个全局执行上下文。
  • 函数执行上下文:函数调用就会创建函数执行上下文,每个函数执行都有自己的函数执行上下文。一段程序中,函数执行上下文,可以有多个。比如函数嵌套调用。
  • eval函数执行上下文: 执行在eval函数中的代码会有属于它自己的执行上下文。

执行上下文栈

js是单线程的,虽然代码中有一个全局执行上下文和多个函数执行上下文。但是同一时间,js只能有其当前时间内的执行上下文。因此内部的所有执行上下文是以执行上下文栈的形式存在。执行的时候this指向当前执行上下文对象。

demo: 以该例子查看函数执行上下文栈。

function c(){
	console.log('yes');
}
function a(){
  function b(){
    c();
  }
  b();
}
a();

image.png

this

this是js关键字,多数情况下,指向 调用他的对象。且在调用时候确认其指向。

此处两个含义:

  1. this是一个对象,即执行上下文对象。
    
  2. this指向调用他的对对象,如果调用他的不是对象,或对象不存在,则会指向全局对象。(严格模式下,调用对象是谁,this就是谁,不管null和undefined)
    

注意: 浏览器环境下,全局对象是window对象,node环境下,全局是global对象。

证明2demo:

function fn1 () {
    console.log(this)
}
function fn2 () {
    'use strict'
    console.log(this)
}
fn1() // window 非严格模式,全局调用
fn2() // undefined  严格模式,全局调用,没用window.f2()

this指向

普通函数中,this指向一般分为以下四种情况:全局绑定、隐式绑定、显式绑定、new构造函数。

最近调用环境是谁,this就是谁,不管嵌套几层,怎么赋值

this指向最近调用他的对象

全局绑定

var num = 1
var foo = {
    num: 10,
    fn: function() {
       console.log(this)
       console.log(this.num)
    }
}
var fn1 = foo.fn // fn1是 function() {
                          // console.log(this)
                         //  console.log(this.num)
                         // }
fn1() // 全局调用,this就是window。this.num=1

隐式绑定

声明在对象的某个属性下的函数,调用时可能考虑这个问题。遵旨是: 不管函数嵌套在哪个obj下,不管怎么调用,记住 最近的调用环境是谁,this就是谁

var a = 'hello'

var obj = {
    a: 'world',
    fn: function() {
        console.log(this.a)
    }
}

obj.fn() // obj下调用的,world

改造:

var a = 'hello'

var obj = {
    a: 'world',
    fn: function() {
        console.log(this.a)
    }
}
var fn1 = obj.fn

fn1() // 全局调用的,hello

嵌套多层:

const obj1 = {
    text: 1,
    fn: function() {
        return this.text
    }
}
const obj2 = {
    text: 2,
    fn: function() {
        return obj1.fn()
    }
}
const obj3 = {
    text: 3,
    fn: function() {
        var fn = obj1.fn
        return fn()
    }
}
// 遵旨:包含this的函数,最终由哪里调用
console.log(obj1.fn()) // 1
console.log(obj2.fn()) // 1
console.log(obj3.fn()) // undefied
  1. obj1.fn()fn中有this,fn由obj1调用,this是obj1,this.text = obj1.text = 1
  2. obj2.fn()中本身没有this,执行的是obj1.fn(),有this,this指向最近的调用obj1,this.text = obj1.text=1
  3. obj3.fn()中本身没有this,执行的方法是obj1.fn,调用时全局调用,this=window,window.text=undefied

充分证明this指向最近调用他的对象

显示绑定

this指向的是对象。显式绑定就是明显的指定this指向的对象。一般通过call,apply,bind方式实现。 语法:

var obj = {}
function fn(args1,args2) {
    console.log(this)
}
fn(args) // window
fn.call(obj, args1,args2) // obj
fn.apply(obj, [args1,args2]) // obj
fn.bind(obj,args1,args2)() // obj
// bind 相当于
var newFn = fn.bind(obj)
newFn(args1,args2) // obj

image.png

  • call:直接调用函数,第一个参数,指定的this指向对象,从第二个参数开始是函数参数,拍平形式传递
  • apply:直接调用函数,第一个参数,指定的this指向对象,第二个参数函数参数 的数组形式。
  • bind: 返回一个改变this指向的心函数,需要手动调用,调用函数的参数拍平

显示绑定,如果传递的指向对象(第一个参数)是null和undefined,那么this就指向window

不是默认绑定

不是默认绑定

不是默认绑定

image.png

显式绑定,多次调用,this指向问题

结论:不管给函数 bind 几次,fn 中的 this 永远由第一次 bind 决定

那如果对一个函数进行多次 bind,那么上下文会是什么呢?

let a = {}
let fn = function () { 
  console.log(this) 
}
fn.bind().bind(a)() 

这里会输出 a吗?可以把上述代码转换成另一种形式:

// fn.bind().bind(a) 等于
let fn2 = function fn1() {
  return function() {
    return fn.apply()
  }.apply(a)
}
fn2()

可以发现,不管给函数 bind 几次,fn 中的 this 永远由第一次 bind 决定,所以结果永远是 window

let a = { 
  name: 'CUGGZ' 
}
function fn() {
  console.log(this.name)
}
fn.bind(a)() // CUGGZ

new 构造函数

new构造函数中,this指向的是实例

function Person(name,age){
  this.name = name;
  this.age = age;
  this.say = function(){
    console.log(this.name + ":" + this.age);
  }
}
var person = new Person("lucy",18);
console.log(person.name); // lucy
console.log(person.age);  // 18
person.say(); // lucy:18

因为 new的过程:

  • 创建新对象
  • 新对象的隐式原型=构造函数的显示原型
  • 执行构造函数,指定this为新对象
  • 根据执行结果的类型,判断返回值(引用类型,返回执行结果;不是引用类型,返回新对象)

this优先级

结论:new > 显示绑定 > 隐式绑定 > 全局绑定

  • 显示>隐式 demo
function foo (a) {
    console.log(this.a)
}
const obj1 = {
    a: 1,
    foo: foo
}
const obj2 = {
    a: 2,
    foo: foo
}
obj1.foo.call(obj2) // 同时存在隐式&显示,显示优先级高,this为obj2,结果为obj2.a = 2
obj2.foo.call(obj1) // 同时存在隐式&显示,显示优先级高,this为obj1,结果为obj1.a = 1
  • 显示 > 全局 demo
function foo (a) {
    this.a = a
}
const obj1 = {}
var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a) // 2
  • new > 显示
var baz = new bar(3) // baz = new foo.bind(obj1) 构造函数和显示同时存在,构造函数优先级高,foo中的this为baz,因此baz.a = 3
console.log(baz.a) // 3

this指向的判断流程图

image.png

特殊this指向

箭头函数

箭头函数的this会在根据其声明位置确定,指向包裹这个箭头函数的第一个普通函数的的调用方

const foo = {  
    fn: function () {  
        setTimeout(function() {  
            console.log(this)
        })
    }  
}  
console.log(foo.fn())
// window

数组遍历方法

数组的遍历方法,foreach/some/filter/map/ every等方法中,可以传递两个参数,一个是每个item执行的操作function(item, index,arr),另一个是 function中this的绑定对象。一般我们省略第二个参数,那么此时this是undefined,就会指向window

箭头函数没有this,既不能显示绑定this,也不能new

分function的类型:普通 or 箭头

  • 普通

image.png 如果操作函数是普通函数,那么该函数中的this,就会是第二个参数对象

  • 箭头 image.png 如果操作函数是箭头函数,那么不存在这个对象,第二个参数不生效。仍然返回数组本身

箭头函数嵌套

function a() {
  return () => {
    return () => {
      console.log(this)
    }
  }
}
console.log(a()()())

由于箭头函数本身没有 this箭头函数中出现的this的 this 只取决包裹箭头函数的第一个普通函数的 this。在这个例子中,因为包裹箭头函数的第一个普通函数是 a,所以此时的 thiswindow。即:箭头函数中的this指向包裹箭头函数的的第一个普通函数的调用方。

立即执行函数表达式

立即执行函数就是定义后立刻调用的匿名函数:

var name = 'hello'
var obj = {
  name: 'world',
  sayHello: function() {
    console.log(this.name)
  },
  hello: function() {
    (function(cb) {
      cb()
    })(this.sayHello)
  }
}
obj.hello() // hello

obj.hello()执行,本质上是(function(cb) { cb() })(this.sayHello)立即执行函数表达式执行,他没有名字,一般是定义就执行,不存在在某个对象里,打点执行,因此其中的this是window。

官方解释:
执行结果是 hello,是 window.name 的值。立即执行函数作为一个匿名函数,通常就是直接调用,而不会通过属性访问器(obj.fn)的形式来给它指定一个所在对象,所以它的 this 是确定的,就是默认的全局对象 window

setTimeout & setInterval

这两个,不管嵌入在什么函数下执行,只要cb是普通函数,由于调用这两个方法的永远是window,因此他们回调函数中的this永远是window

var name = 'hello'
var obj = {
  name: 'world',
  hello: function() {
    setTimeout(function() {
      console.log(this.name)
    })
  }
}
obj.hello() // 执行的是延时函数,且延时函数回调是普通函数,那么cb中this是window。hello

改造箭头函数:

var name = 'hello'
var obj = {
  name: 'world',
  hello: function() {
    setTimeout(() => {
      console.log(this.name)
    })
  }
}
obj.hello() // cb是箭头函数,this指向包裹这个箭头的第一个普通函数的调用方,第一个普通函数hello(),调用方obj.hello()因此this=obj.   world

题目

github.com/whylisa/fro…