函数和执行上下文

聊函数之前需要先讲一讲javascript的执行顺序。因为函数的存在会让js的执行顺序变得更加复杂。js默认的执行顺序是从上到下,但是由于执行上下文的存在,执行顺序会和想象中的不太一样。

变量提升

javascript引擎会把变量的声明部分和函数的声明部分提升到代码开头,并给予undefined的默认值。变量和函数的声明会在物理层面移动到代码的最前面,在js执行上下文的工作方式中,声明会先被提升,也就是在所谓的预编译阶段,js变量对象(AO对象)会被创建,供js引擎使用

输入一段代码,经过编译后,会生成两部分内容:执行上下文和可执行代码,执行上下文是JavaScript执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如this、变量、对象以及函数,执行上下文中包括变量环境和词法环境两个部分,块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现。

栈和栈溢出

在执行js代码时,可能会存在多个执行上下文,js引擎通过栈对执行上下文进行管理,栈是一种后进先出的数据结构

执行上下文创建后之后,js引擎会将执行上下文压入栈中,这种用来管理执行上下文的栈成为执行上下文栈,又被称为调用栈,它是js引擎追踪函数执行的一个机制,当一次有多个函数被调用时,通过调用栈能够追踪到哪个函数正在被执行以及各函数之间的调用关系。当函数被调用时,会为其创建执行上下文,将其压入调用栈,执行完毕返回时对应的执行上下文会从栈顶弹出

调用栈是有大小的,当入栈的执行上下文超过一定数目,JavaScript引擎就会报错,我们把这种错误叫做栈溢出

函数的一些相关概念

函数有两种定义方式,函数表达式以及函数声明

函数表达式:const func=function(){}

函数声明:function func(){}

两种形式在使用上没什么影响,但是函数表达式不会提升,所以不能在定义之前调用,函数声明会被提前到外部脚本或者外部作用域的顶部,就是说js会把函数声明的变量提升,所以在定义之前就能被调用;函数表达式的话,js会先执行变量提升,为func先赋予undefined,等执行到定义的时候,再将对应的函数赋给变量,因此在定义之前执行,func是undefined。

立即执行函数(IIFE):在声明之后立即调用的函数表达式

(function(){
	statements;
}())

arguments:arguments是一个对应于传递给函数的参数的类数组对象(非箭头函数下)。就是把所有的入参变成一个类似数组的对象集合,这样的话就能十分愉快的对入参进行统一处理,可以通过Array.from()方法和扩展运算符将它转为真实的数组。

箭头函数:箭头函数语法比函数表达式更加简洁,没有自己的this(不等于没有this),arguments,super或者new.target(允许检测函数或者构造方法是否通过new运算符被调用)

箭头函数更适用于本来就需要匿名函数的地方,而且它不能用作构造函数

箭头函数不会创建自己的this,但是会从自己的作用域链的上一层继承this,箭头函数并不会创建其自身的执行上下文,所以箭头函数中的 this 取决于它的外部函数

作用域

作用域决定了变量,常量和参数被定义的时间和位置,当某个变量的作用域是一个给定的函数时,每次函数调用开始时,形参才会被实参赋值,参数才真实存在,在函数返回后,参数就失去作用域了。因为函数的调用和作用域关系比较紧密,所以将作用域作为函数的一部分分析

js的作用域包括全局作用域,函数作用域和块级作用域(es6之后通过let,const才生成的)三个部分。

全局作用域顾名思义就是在声明在全局环境下的一切,全局作用域下声明的变量是全局变量,全局变量意味着在整个程序中哪里都可以用,不好的地方是,这也意味着哪里都可以进行修改,如果某一个部分不小心修改了全局变量,其他引用全局变量的地方就会被影响。

函数作用域其实可以看成是特殊的块级作用域,之前有提过,块表达式是以闭合的大括号作为标识符,函数也是如此。但是在es6之前,由于只有一个var关键字进行变量声明,因此将函数作用域作为特殊的块级作用域来处理,在函数作用域中,变量在声明它们的函数体以及这个函数体嵌套的任意函数体中都是有意义的,这个时候就可能会出现一些不可预测的问题,比如传说中的for循环:

function test(){
	for(var i = 0; i < 10;i++){
    setTimeout(function () {
      console.log(i); // 隔一秒输出一个10,因为var这个变量被提升了
    }, 1000 * i);
  }
}

// 优化方案1
function test() {
  for (var i = 0; i < 10; i++) {
    (function(item) {
    setTimeout(function () {
      console.log(item);
    }, 1000 * item);
    })(item)
  }
}
// 优化方案2
function test(){
	for(let i = 0; i < 10;i++){
    setTimeout(function () {
      console.log(i); // 隔一秒输出一个10,因为var这个变量被提升了
    }, 1000 * i);
  }
}


var scope = 'global';
function func(){
	console.log(scope); // undefined
  var scope = 'local';
  console.log(scope) // local
}

// 因为var会存在声明提前的问题,函数在预编译过程中会变成下面这样
var scope = 'global';
function func(){
  var scope;
	console.log(scope); // undefined
  scope = 'local';
  console.log(scope) // local
}

这个问题出现的原因就是之前讲过的变量提升,函数内部通过var声明的变量在编译阶段都会被存放到变量环境里,而let和const声明的变量,在编译阶段会被存放到词法环境中,当进入函数的作用域块时,作用域块中通过let声明的变量,都会被存放在词法环境的一个单独区域中。

执行作用域块时,首先先去词法环境的栈顶自上而下查询,如果找到了就返回,没找到就往变量环境中继续查栈

作用域创建的过程:

  • 创建AO对象
  • 找形参和变量声明,赋予undefined(函数与变量相比,会被优先提升,所以函数会被提升到更靠前的位置
  • 实参和形参相统一
  • 找函数声明覆盖变量的声明(这里指的是函数表达式声明的函数,如果和变量声明重名的话,变量声明会被覆盖掉)

词法作用域:作用域是由代码中函数声明的位置来决定的,他们在代码阶段就已经决定好了,与函数是怎么调用没有关系,比如下面的代码,函数a和b都是全局声明的函数,所以他们的词法作用域在全局,当a函数执行上下文中没有变量时,就会找全局声明的变量

function a(){
  console.log(demo)
}

function b(){
  var demo ="lalala"
  a()
}
var demo = "hahaha"
b() // 'hahaha'

作用域链:是一个对象列表或者链表,定义了这段代码作用域中的变量,找某个变量时如果在当前作用域下没有找到,会沿着链式的作用域链查找,而不能从父作用域引用子作用域中的变量和引用(意思就是只能找爸爸,爷爷还有更远的祖宗,不能找兄弟,不能找爷爷的兄弟)

闭包

闭包是指有权访问另一个函数作用域中的变量的函数,闭包可以使得在一个内层函数中访问到其外层函数的作用域

词法作用域根据源代码中声明变量的位置来确定该变量在何处可用。嵌套函数可访问声明于它们外部作用域的变量。
闭包是一个函数和对其周围状态(词法环境)的引用捆绑在一起。为了实现闭包,不能用动态作用域的动态堆栈来存储变量,因为这样的话,函数返回的时候变量就需要出栈,然后就会被回收,所以其实外部环境的闭包数据被存在堆里面,所以就算它的执行上下文也已经出栈,内部的变量也会一直存在。 JS 引擎判断当前是一个闭包时,就会在堆空间创建换一个“closure(foo)”的对象(这是一个内部对象,JS 是无法访问的),用来保存 对应变量

闭包的理解:

  • 在调用js函数对象时,会创建一个执行上下文以及对应的作用域链

  • 在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量

  • 如果函数中嵌套了函数,被嵌套的函数就会复制父函数的作用域链并保存,当嵌套函数(也就是我们说的闭包)被返回的时候,就会连同这个作用域一起被保留下来

  • 当这个闭包被引用的时候,会有个外部引用指向这个嵌套函数,它就不会被当作垃圾回收

    function father() {
      let num = 11;
      return function son() {
        console.log(num);
      };
    }
    father()(); // 11
    

    用一个最简单的例子来进行讲解:

    • father函数里面有两个活动对象,num和son
    • 在son里面,执行num的时候,自身没有num,就会从作用域链中去找,找到上一层father中包含num,于是将它添加到自己的作用域链中
    • 在执行father()()的时候,虽然最终看起来是在执行son函数,但是此时son函数的作用域链中已经保存了father中的num,所以num会被成功输出

闭包的常用例子

// 防抖
function debounce(delay, cb) {
  let timer;
  return function (val) {
    clearTimeout(timer);
    timer = setTimeout(() => cb(val), delay);
  };
}

// 节流
function throttle(delay, cb) {
  let timer;
  return function (arg) {
    if(!timer){
      timer = setTimeout(() => {
      cb(arg);
      timer = null;
    }, delay);
    }
    
  };
}
// 柯里化:把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数并返回结果的新函数,简单的说就是把多个入参的函数改为每次都单一入参,返回新入参的函数再不断调用
// 柯里化的优点是能够进行参数复用,能提前确认(不同if条件下可以返回不同的函数)

// bind方法实际就用了柯里化的思想
function bind(f,o){
  if(f.bind) return f.bind(o);
  else return function(){
    return f.apply(o,arguments)
  }
}
this

理解了作用域和执行上下文之后,this就会很好理解,this是当前执行上下文(global,function,eval)的一个属性,要注意以下几点:

  • 在函数中直接使用,指向Windows(函数声明是在全局作用域下的,所以函数中的执行上下文是windows)

  • 作为对象方法被调用时,指向该方法,谁调用我就指向谁(指向执行时的上下文)

  • 箭头函数的this,函数自身不会有this

    • 在定义函数的时候绑定(在哪声明就指向哪)
    • 内部的this就是外层代码块的this,所以无法作为构造函数(构造对象会将构造之后形成的新对象作为其调用上下文)
    • this是继承父执行上下文中的this
  • 派生类的构造函数没有初始的this绑定,这个时候需要调用super()来生成this绑定

  • 通过构建函数new关键字生成一个实例对象,此时this指向这个实例对象

  • 可以通过call,apply以及bind改变this的指向

    const name = '啦啦啦'const age = 18;
    const obj = {
      name:'呵呵呵',
      age:this.age;
      fn:function(a,b){
            console.log(this.name+"-"+this.age+"-"+ a +"-"+ b)
      }
    }
    const demo = {
      name:'嘿嘿嘿',
      age:20
    }
    obj.myFun.call(demo,'成都','上海');     // 嘿嘿嘿-20-成都-上海
    obj.myFun.apply(demo,['成都','上海']);      // 嘿嘿嘿-20-成都-上海 
    obj.myFun.bind(demo,'成都','上海')();       // 嘿嘿嘿-20-成都-上海
    obj.myFun.bind(demo,['成都','上海'])();   // 嘿嘿嘿-20-成都, 上海-undefined
    

    call,apply以及bind都是改变this的指向,将函数的this指向括号里的参数,相当于调用函数的时候this指的就是demo,call和apply只是传参不同,apply要传数组。

    bind不是立即执行参数,call和apply是立即执行,所以要立即执行bind的话需要加一个()

    Function.prototype.bind = function(thisArg) {
      if (typeof this !== 'function') {
        return;
      }
      var _self = this;
      var args = Array.prototype.slice.call(arguments, 1) //从第二个参数截取
      return function() {
        return _self.apply(thisArg, args.concat(Array.prototype.slice.call(arguments))); // 注意参数的处理
      }
    }
    
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值