js理解---执行上下文、作用域、作用域链、闭包、this指向

导读: 执行上下文、作用域、作用域链、闭包、this指向这几个方面之前的联系还是比较强的,在这里就统一的整理一下

执行上下文
作用域
作用域链
闭包
this指向

执行上下文

在一段 JS 脚本执行之前,要先解析代码(所以说 JS 是解释执行的脚本语言),解析的时候会先创建一个 全局执行上下文 环境 ,在这个环境中,所有变量提升、函数声明提前都会先拿出来,有的直接赋值,有的为默认值 undefined,代码从上往下开始执行,就叫做执行上下文。(这是在代码执行之前开始的工作)
 
在 JavaScript 的世界里,运行环境有三种,分别是:
 1. 全局环境:代码首先进入的环境
 2. 函数环境:函数被调用时执行的环境
 3. eval函数:https://www.cnblogs.com/chaoguo1234/p/5384745.html(不常用)

特点:

  1. 单线程,在主进程上运行
  2. 同步执行,从上往下按顺序执行
  3. 全局上下文只有一个,浏览器关闭时会被弹出栈
  4. 函数的执行上下文没有数目限制
  5. 函数每被调用一次,都会产生一个新的执行上下文环境

注意! 执行全局代码时,会产生一个执行上下文环境,每次调用函数都又会产生执行上下文环境。当函数调用完成时,这个上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。处于活动状态的执行上下文环境只有一个。



作用域

ES6 之前 JS 没有块级作用域,只有全局作用域函数作用域

作用域就是一个独立的地盘,让变量不会外泄、暴露出去,也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

函数作用域:顾名思义就是在这个函数体里边才能访问的变量;

var a = 100
function fn() {
    var a = 200
    console.log('fn', a) //200
}
console.log('global', a) //100
fn()

块级作用域:ES6新增,用let命令新增了块级作用域,外层作用域无法获取到内层作用域,非常安全明了。即使外层和内层都使用相同变量名,也都互不干扰;

if (true) {
    let name = 'zhangsan'
}
console.log(name)  // 报错,因为let定义的name是在if这个块级作用域


作用域链

首先认识一下什么叫做 自由变量 。如下代码中,console.log(a)要得到a变量,但是在当前的作用域中没有定义a(可对比一下b)。当前作用域没有定义的变量,这成为 自由变量 。自由变量如何得到 —— 向父级作用域寻找。

var a = 100
function fn() {
    var b = 200
    console.log(a)
    console.log(b)
}
fn()

如果父级也没呢?再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是 作用域链

var a = 100
function F1() {
    var b = 200
    function F2() {
        var c = 300
        console.log(a) //100 自由变量,顺作用域链向父作用域找
        console.log(b) //200 自由变量,顺作用域链向父作用域找
        console.log(c) //300 本作用域的变量
    }
    F2()
}
F1()



闭包

闭包的定义其实很简单:函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包

function A() {
  let a = 1
  window.B = function () {
      console.log(a)
  }
  通常我们也喜欢这么写
  return function (){
  console.log(a)
  }
  a=null
}
A()
B() // 1

很多人对于闭包的解释可能是函数嵌套了函数,然后返回一个函数。其实这个解释是不完整的,就比如我上面这个例子就可以反驳这个观点。没有返回函数而是将函数挂载到window上。

在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。

缺点是造成内存泄露,解决办法是使用完变量之后将其设置为null。

经典面试题,循环中使用闭包解决 var 定义函数的问题

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, 1000)
}

首先因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。

解决方法:第一种是使用闭包的方式

for (var i = 1; i <= 5; i++) {
  ;(function(j) {
    setTimeout(function timer() {
      console.log(j)
    }, 1000)
  })(i)
}

在上述代码中,我们首先使用了立即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j 上面不会改变,当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的。

解决方法:第二种就是使用 setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入。

for (var i = 1; i <= 5; i++) {
  setTimeout(
    function timer(j) {
      console.log(j)
    },
    1000,
    i
  )
}

解决方法:第三种就是使用 let 定义 i 了来解决问题了,这个也是最为推荐的方式

for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, 1000)
}


this指向

涉及面试题:如何正确判断 this?箭头函数的 this 是什么?

先搞明白一个很重要的概念 —— this的值是在执行的时候才能确认,定义的时候不能确认! 为什么呢 —— 因为this是执行上下文环境的一部分,而执行上下文需要在代码执行之前确定,而不是定义的时候。

我们先来看几个函数调用的场景

function foo() {
  console.log(this.a)
}
var a = 1
foo()

const obj = {
  a: 2,
  foo: foo
}
obj.foo()

const c = new foo()

接下来我们一个个分析上面几个场景

  • 对于直接在全局调用 foo 来说,不管 foo 函数被放在了什么地方,this 一定是 window
  • 对于 obj.foo() 来说,我们只需要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo 函数中的 this 就是 obj 对象
  • 对于 new 的方式来说,this 被永远绑定在了 c 上面,不会被任何方式改变 this

改变this指向的call、apply、bind 方法使用

由于js 中this的指向受函数运行环境的影响,指向经常改变,使得开发变得困难和模糊,call、apply、bind基本都能明了的绑定this的指向。

call

call 方法可以指定this 的指向(即函数执行时所在的的作用域),然后再指定的作用域中,执行函数

	var name='globalName';
    var age='globalAge';
    var obj = {
      name:'qfl',
      age:'26'
    };
    var f = function () {
      return this.name+' '+this.age
    };
    f(); // globalName globalAge
    f.call(obj) // qfl 26

call 可以接受多个参数,第一个参数是this 指向的对象,之后的是函数回调所需的入参

func.call(thisValue, arg1, arg2, ...)

在全局调用的时候,第一个参数是下面几种情况时,指向的都是window全局 //globalName globalAge

  • f.call()
  • f.call(null)
  • f.call(undefined)
  • f.call(window)
  • f.call(this)

apply

apply 和call 作用类似,也是改变this 指向,然后调用该函数,唯一区别是apply 接收数组作为函数执行时的参数

func.apply(thisValue, [arg1, arg2, ...])

apply方法的第一个参数也是this所要指向的那个对象,如果设为null或undefined,则等同于指定全局对象。

第二个参数则是一个数组,该数组的所有成员依次作为参数,传入原函数。

function f(x, y){
  console.log(x + y);
}

f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2

bind

bind 用于将函数体内的this绑定到某个对象,然后返回一个新函数
(call/apply则是可以指定this 的指向(即函数执行时所在的的作用域),然后再指定的作用域中,执行函数)

var d = new Date();
d.getTime() // 1481869925657

var print = d.getTime;
print() // Uncaught TypeError: this is not a Date object.

报错是因为,d.getTime 赋值给 print 后,getTime 内部的this 指向方式变化,已经不再指向date 对象实例了

var print = d.getTime.bind(d);
print() // 1481869925657

bind 接收的参数就是所要绑定的对象

var add = function (x, y) {
  return x * this.m + y * this.n;
}

var obj = {
  m: 2,
  n: 2
};

var newAdd = add.bind(obj);
newAdd(5,2) // 14

注意! 多个bind一起使用,生效的始终是第一个bind! (add.bind(obj).bind(window) ===> 指向的始终是obj)

区别:call和apply都是立即执行的,bind会返回一个函数,然后再执行函数才会生效


箭头函数中的this

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

首先箭头函数其实是没有 this 的,箭头函数中的 this 只取决包裹箭头函数的第一个普通函数的 this。在这个例子中,因为包裹箭头函数的第一个普通函数是 a,所以此时的 thiswindow。箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。另外对箭头函数使用 bind 这类函数是无效的。

以上就是 this 的规则了,但是可能会发生多个规则同时出现的情况,这时候不同的规则之间会根据优先级最高的来决定 this 最终指向哪里。

首先,new 的方式优先级最高,接下来是 bind 这些函数,然后是 obj.foo() 这种调用方式,最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。

js理解---执行上下文、作用域、作用域链、闭包、this指向_第1张图片

你可能感兴趣的:(js,javascript)