JavaScript——函数

函数

函数对任何语言来说都是一个核心概念,函数可以封装任意多条语句,可以在任何地方、任何时刻进行调用执行。在学习js的过程中,遇到过闭包、作用域等概念,让人摸不着头脑,在一些面试的笔试题也经常会头疼不知道考察的点。因此写下本篇博客,系统的去学习js中函数的相关概念。

1. 基础概念

在这里插入图片描述

1.1 函数的定义

1.函数声明

//function是关键字
function functionName(arg0, arg1, arg2) {
    //函数体
}

函数声明有一个重要的特——函数声明提升

sayHi(); //Hi!
function sayHi(){
   alert("Hi!")}

2.函数表达式
这种形式看起来很像常规的变量赋值语句,即创建一个函数并将它赋值给functionName。这种情况下创建的函数叫做匿名函数(function后面没有标识符),匿名函数的name属性为空字符串。

var functionName = function(arg0, arg1, arg2) {
   //函数体
}

函数表达式与其他表达式一样,在使用前必须先赋值。

sayHi(); //报错
var sayHi = function(){
   alert("Hi!")}

3.使用构造函数Function
Function类型也是一种引用类型,其他更多引用类型可以看我正在整理的引用类型一文,一般不推荐这种方式去定义函数,因为该方法会导致两次代码的解析(1.解析常规的js代码 2.解析传递入构造函数中的字符串)

var sum = new Function("num1", "num2");

但是通过该方法我们可以很好的理解“函数是对象,函数名是指针”,我们再下面一小节详细讨论。

1.2 函数是对象

1.2.1 函数的内部属性 —— arguments、this

函数内部,有两个特殊的对象:arguments和this

  1. arguments主要用途是保存函数参数,详情可见下面1.3.1小结。但是不要忘记了,arguments是一个类数组对象,该对象有一个叫callee的属性,该属性是一个指针,指向拥有这个arguments对象的函数,详情可见递归一小节。
  2. this引用的是函数在执行的环境对象,有关this我们需要理解他的指向问题,该问题会在闭包中详细讨论。
1.2.2 函数的属性 —— length

length属性表示函数希望接受的命名参数的个数

1.2.3 函数的方法 —— apply()和call()

call()和apply()两个放大都是在特定的作用域中调用函数,实际上等同于设置函数体内部this对象的值。他们主要的区别在于,call()接受的是一个一个的参数,而apply()接受的是一个参数数组

1.3 函数的参数

1.3.1 arguments

js函数不介意传递进来多少参数,也不在乎传递参数的类型。我们需要十分注意函数中的arguments对象,js函数的所有参数都存放在这个arguments对象。

  • 你可以传递人一个参数进函数,不论你在定义函数时规定了多少个参数,因为你传递的参数会依次传递给arguments
  • arguments可以使用方括号访问元素(arguments[0]表示传递进来的第一个参数,依次类推),用length属性确定传递进来多少个参数
  • arguments对象只是与数组类似,但他并不是Array的实例
1.3.2 按值传递

js中的所有函数都是按值传递的。这句话可能不是很容易理解,我们先从一个简单的例子开始理解。

function addTen(number) {
    num += 10
    return number
}
var count = 20
var result = addTen(count)
alert(count) //20
alert(result) //30

在上面的例子中,传递的参数被复制给了一个局部变量,也就是arguments对象中的一个元素,因此不会影响count值。这下就可以大致理解开头那句话的意思了,但是我们知道在js中除了基本数据类型外,还有引用类型值,不了解的可以去看我的另外一篇博客变量、作用域和内存问题
基本类型值的传递在上述例子里我们可以轻松的理解,那么引用类型值的传递呢?

function setName(obj) {
   obj.name = "wk"
}
var person = new Object();
setName(person);
alert(person.name); //wk

在上述代码中,我们在函数体内改变传递的来的引用类型值,在函数体外部也可以体现出来,那是不是就意味着引用类型值的传递不满足“js中的所有函数都是按值传递的”这句话呢?答案式否定的,我们来看下面一个例子。

function setName(obj) {
   obj.name = "wk"
   obj = new Object()
   obj.name = "cy"
}
var person = new Object();
setName(person);
alert(person.name); //wk

我们可以看到,即使我们后来答案并不是cy,因为在我们传递参数时,我们是按值传递的,而引用类型复制传值是将指针传递给arguments中的参数,即便后来给obj一个新的对象,那也不过是修改了arguments[0]参数指针的指向,并不会影响person的指针指向。

1.4 没有重载

js函数没有重载,如果你定义了相同名字的函数,则这个个名字只属于后定义的函数

sayHi();
function sayHi(){
  console.log("A")
}
function sayHi(){
  console.log("B")
}
sayHi();

在这个小例子中,两个sayHi()函数都会打印B。因为函数声明提升,两个定义函数会被放在最顶端,接着根据每有重载这一规则,两个sayHi()函数都会打印B。

2. 递归

递归函数是指一个函数通过名字调用自身的情况。因为递归是通过函数名,而js中的函数声明可以通过函数表达式来声明,因此我们可以使用在1.2.1中提到的arguments的callee属性避免一些不必要的错误。

function factorial(num) {
    if(num < 1) {
        return 1
    } else {
        return num*argument.callee(num - 1)
    }
}

3. 闭包

闭包是指有权访问另一个函数作用域中的变量函数。

3.1 执行环境、变量对象、活动对象、作用域、作用域链

在此部分参考了炼心的博客js基础梳理-究竟什么是变量对象,什么是活动对象?

3.1.1 执行环境(执行上下文)
  1. 执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为。
  2. 全局执行环境是最外围的一个执行环境,在web中,全局执行环境被认为是window对象,因此所有的全局变量和函数都是作为window对象的属性和方法创建的。
  3. 执行环境中的所有代码执行完毕后,该环境会被销毁,保存在其中的变量和函数也会随之销毁;全局执行环境直到应用程序退出,例如关闭浏览器后才会被销毁。
  4. 每个函数都有自己的执行环境,当执行流进入一个函数时,函数的执行环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,将控制权返回给之前的执行环境。
3.1.2 变量对象(Variable Object,VO)
  1. 每个执行环境都有一个与之关联的变量对象,执行环境中定义的所有变量和函数都保存在这个对象中。
3.1.3 活动对象(Active Object,AO)
  1. 只有全局执行上下文的变量对象允许通过VO的属性名称来间接访问,在其他执行上下文中是不能直接访问VO对象的。

  2. 未进入执行阶段前,变量对象中的属性都不能访问!但是进入到执行阶段之后,变量对象(VO)转变成了活动对象(AO),里面的属性都能被访问了,然后开始进行执行阶段的操作。

  3. 因此,对于函数上下文来讲,活动对象(AO)与变量对象(VO)其实都是同一个对象,只是处于执行上下文的不同生命周期。不过只有处于执行上下文栈栈顶的函数执行上下文中的变量对象,才会变成活动对象。

3.1.4 作用域、作用域链
  1. 当代码在一个执行环境中执行时,会创建变量对象的一个作用域链。作用域链的作用时保证执行环境有权访问的所有变量和函数有序的访问。
  2. 作用域链的前端始终都是当前执行的代码所在的环境变量。如果这个环境是函数,则将其活动对象作为变量对象。

3.2 执行期上下文

3.2.1 执行上下文的重要属性

此处参考玩弄心里的鬼的博客解密 JavaScript 执行上下文

  • 变量对象
  • 作用域链
  • this

1.变量对象

  • 全局上下文中的变量对象其实就是全局变量对象(VO)。在3.1节我们提到了全局变量对象可以直接访问。我们可以通过this来访问全局对象,并且在浏览器环境中,this === window;在node环境中,this === global。
  • 在函数上下文中的变量对象,我们用活动对象(AO)表示,在3.1节我们提到了只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,并且只有被激活的变量对象,其属性才能被访问,其本质和变脸对象是一个对象,只是处于不同的时期。

2.作用域链

  • 当代码在一个执行环境中执行时,会创建变量对象的一个作用域链。作用域链的作用时保证执行环境有权访问的所有变量和函数有序的访问。当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级执行上下文的变量对象中查找,一直找到全局上下文的变量对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
  • 作用域链的前端始终都是当前执行的代码所在的环境变量。如果这个环境是函数,则将其活动对象作为变量对象

3.this

  • 在全局执行上下文中,this的指向全局对象。(在浏览器中,this引用 Window 对象)。
  • 在函数执行上下文中,this 的值取决于该函数是如何被调用的。如果它被一个引用对象调用,那么this会被设置成那个对象,否则this的值被设置为全局对象或者undefined(在严格模式下)

总结起来就是,谁调用了,this就指向谁。

有关this的指向问题可以参考Javascript中的this这篇博客。

3.2.2 js运行三部曲

在此部分参考了二郎神杨戬的博客JS进阶系列-JS执行期上下文(一)这篇文章写的很清楚,在此只做一个总结,详细可以去看这篇博客进行了解。

在了解闭包之前,我们最好先弄明白执行期上下文的生命周期,而在了解执行期上下文前我们又需要先了解一下js的三个运行步骤:语法分析、预编译、解释执行

  • 语法分析,检查是否出现语法的错误
  • 预编译,全局预编译,函数体预编译
  • 解释执行,一行一行的边解释边执行
3.2.3 预编译

前提了解:

  1. imply global 暗示全局变量,即任何变量,如果变量未经声明就赋值,此变量就为全局对象所有
  2. 一切声明的全局变量都是window属性

1.函数预编译(函数执行期上下文)

  • 创建AO对象
  • 找形参和变量声明,将变量的形参名作为AO的属性名,值为undefined
  • 将实参值和形参进行统一
  • 在函数体里面找函数声明,值赋予函数体

2.全局预编译(全局执行期上下文)

  • 创建GO对象
  • 找变量声明,值为undefined
  • 找函数声明,值赋予函数体

3.3 回到闭包

如果详细的了解了3.1、3.2节总结的概念,我们就能轻松的理解变量的作用域和变量的生命周期。一个函数可以沿着作用域链逐层的向上访问外部的变量,直到搜索到全局对象位置,而外部的环境却无法访问内部环境的变量。而且,函数内部的局部变量在函数执行完毕后会被销毁。基于以上两点,在此引出闭包的概念,在此重复一遍开头提到的那句话可能你已经豁然开朗,闭包是指有权访问另一个函数作用域中的变量函数。理解了概念后就是要去实现闭包!

3.3.1 原理

在一个函数的内部定义的函数会将会将外部函数的活动对象添加到它的作用域链中。即a函数内部创建了一个b函数,则a的活动对象会添加到b的作用域链中。如果此时我们将b函数作为a函数的返回值返回给一个全局表里,而b函数的作用域链的中从上到下分别包含了自身的活动对象、a的活动对象、全局变量对象,这样在a函数执行结束后,其活动对象也不会被销毁,因为b函数任然可以引用这个活动对象。

3.3.2 闭包和变量
function createFunction() {
   var result = new Array();
   for (var i=0; i<10; i++) {
       result[i] = function() {
           return i;
       }
   }
   return result
}

这个函数返回的result数组中的每个函数最后输出的结果都是10,因为在执行creatFunction()这个函数的活动对象i的最后值为10.

function createFunction() {
   var result = new Array();
   for (var i=0; i<10; i++) {
       result[i] = function(num) {
           return function(num) {
                return num
           }
       }
   }
   return result
}

在修改后的函数中,我们将每个i的值又保存在了result[i]的每个函数的活动对象中,这样result[i]内部返回的匿名函数又形成了一个闭包,依次拥有自身的活动对象、result[i]的活动对象、createFunction()的活动对象和全局环境的变量对象,这样就可以返回1,2,…,9

3.3.3 this

var name = "wk"
var object = {
    name: "cy"
    getName: function(){
       return function(){
          return this.name  //wk
       }
    }
}

这里不会输出cy,因为内部函数在沿着作用域链搜索的时候始终是从自身的活动对象开始,而自身的活动对象拥有this这个变量,this这个对象是运行时基于函数的执行环境绑定的:在全局函数中,this为window、作为莫格对象的时候,this为该对象。有关this的指向问题可以参考3.2.1节中的描述。

3.4 闭包的作用

敬请期待

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值