执行上下文
概念
在js中,执行上下文是代码运行的环境。代码只要执行,就有其执行上下文。执行上下文中有:
- 变量环境:编译阶段,var变量提升,以及执行阶段var变量存储和赋值。
- 词法环境:编译阶段,let变量放在词法环境,代码执行到块级作用域下,形成词法环境栈,对该块级下let变量存储、赋值。执行完,词法环境中,该块级词法栈出栈。
- this:this与执行上下文绑定,每个执行上下文都有this.
- 外部环境
分类
执行上下文分为:全局&函数执行上下文。
- 全局执行上下文:任何不在函数内部执行的代码,在执行中就会创建全局执行上下文,这类代码在执行时,会首先创建window对象,然后将this指向window,一个程序中只有一个全局执行上下文。
- 函数执行上下文:函数调用就会创建函数执行上下文,每个函数执行都有自己的函数执行上下文。一段程序中,函数执行上下文,可以有多个。比如函数嵌套调用。
eval函数执行上下文: 执行在eval函数中的代码会有属于它自己的执行上下文。
执行上下文栈
js是单线程的,虽然代码中有一个全局执行上下文和多个函数执行上下文。但是同一时间,js只能有其当前时间内的执行上下文。因此内部的所有执行上下文是以执行上下文栈的形式存在。执行的时候this指向当前执行上下文对象。
demo: 以该例子查看函数执行上下文栈。
function c(){
console.log('yes');
}
function a(){
function b(){
c();
}
b();
}
a();
this
this是js关键字,多数情况下,指向 调用他的对象。且在调用时候确认其指向。
此处两个含义:
-
this是一个对象,即执行上下文对象。 -
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
- obj1.fn()fn中有this,fn由obj1调用,this是obj1,this.text = obj1.text = 1
- obj2.fn()中本身没有this,执行的是obj1.fn(),有this,this指向最近的调用obj1,this.text = obj1.text=1
- 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
- call:直接调用函数,第一个参数,指定的this指向对象,从第二个参数开始是函数参数,拍平形式传递
- apply:直接调用函数,第一个参数,指定的this指向对象,第二个参数函数参数 的数组形式。
- bind: 返回一个改变this指向的心函数,需要手动调用,调用函数的参数拍平
显示绑定,如果传递的指向对象(第一个参数)是null和undefined,那么this就指向window
不是默认绑定
不是默认绑定
不是默认绑定
显式绑定,多次调用,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指向的判断流程图
特殊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 箭头
- 普通
如果操作函数是普通函数,那么该函数中的this,就会是第二个参数对象
- 箭头
如果操作函数是箭头函数,那么不存在这个对象,第二个参数不生效。仍然返回数组本身
箭头函数嵌套
function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())
由于箭头函数本身没有 this ,箭头函数中出现的this的 this 只取决包裹箭头函数的第一个普通函数的 this。在这个例子中,因为包裹箭头函数的第一个普通函数是 a,所以此时的 this 是 window。即:箭头函数中的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