函数的this、Call Stack、作用域初体验

886 阅读10分钟

1 函数的5种声明

1.1 具名函数

  function 函数名(参数1,参数2,……,参数n){
    函数体
  }
  //例子
  function f(x,y){
    return x+y
  }
  f.name // 'f' 
  // .name是函数的一个属性

1.2 匿名函数

  var 函数名;
  函数名 = function(参数1,参数2,……,参数n){
    函数体
  }
  //例子
  var f
  f = function(x,y){
    return x+y
  }
  f.name // 'f'

1.3 具名函数赋值

  var 函数名;
  函数名 = function 假函数名(参数1,参数2,……,参数n){
    函数体
  }
  //例子
  var f
  f = function f2(x,y){ return x+y }
  f.name // 'f2'
  console.log(f2) // f2 is not defined,f2连声明都没有

1.4 window.Function

  var 函数名 = new Function('参数1','参数2','……','参数n','函数体')
  //例子
  var f = new Function('x','y','return x+y')
  f.name // "anonymous"

这种方式的参数全部为字符串形式,除最后一个参数是函数的主体之外,其余参数全是参数

1.5 箭头函数

  var 函数名 = (参数1,参数2,……,参数n) =>{
    函数体
  }
  //如果参数只有一个,那么参数的括号可以省略
  //如果函数体只有一句,那么函数体的括号也可以省略
  //例子
  var f = (x,y) => {
     return x+y
  }
  var sum = (x,y) => x+y
  var n2 = n => n*n

2 调用函数

在JavaScript中,函数有几种调用方式

  • 函数调用模式
  函数名(参数1,参数2,……,参数n);
  • 构造函数调用模式
  变量.函数名(参数1,参数2,……,参数n);
  • call调用模式
函数名.call(参数0,参数1,参数2,……,参数n);

其实前两种都是语法糖,最后一种函数调用模式才是正常的调用形式

前两种都可以改写为第三种模式

  函数名(参数1,参数2,……,参数n); //等价于
  函数名.call(undefined,参数1,参数2,……,参数n);

  变量.函数名(参数1,参数2,……,参数n); //等价于
  变量.函数名(变量,参数1,参数2,……,参数n);

3 arguments、this是个什么东西

函数名.call(参数0,参数1,参数2,……,参数n);

以这个语句为例

3.1 arguments

arguments就是由参数1参数n组成的一个伪数组,有key-valuelength,但是其__proto__不指向Array.prototype

3.2 this

这里只简单的讲一下this在函数调用中代表着什么

this其实就是参数0,其数据类型根据参数0数据类型的不同而不同

这里要分两种情况普通模式严格模式

  • 普通模式
  var a;
  function f(){
  a = this;
  console.log(typeof(a));
  console.log(this)
  }

  f.call(undefined,1,2)
  //object
  //Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}

  f.call(null,1,2)
  //object
  //Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}

  f.call(1,1,2)
  //object
  //Number {1}

  f.call('s',1,2)
  //object
  //String {"s"}

  f.call(true,1,2)
  //object
  //Boolean {true}

  f.call({},1,2)
  //object
  //{}

  f.call(function(){},1,2)
  //function
  //ƒ (){}

  f.call([1,2],1,2)
  //object
  //(2) [1, 2]

参数0undefined或者null时,浏览器会将this自动变成window
参数0为基本类型时,this会变成复杂类型,就是其__proto__指向数据类型.protortpe,其值为参数0

  • 严格模式
  var a;
  function f(){
  "use strict";
  a = this;
  console.log(typeof(a));
  console.log(this)
  }

  f.call(undefined,1,2)
  //undefined
  //undefined

  f.call(null,1,2)
  //object
  //null

  f.call(1,1,2)
  //number
  //1

  f.call('s',1,2)
  //string
  //s

  f.call(true,1,2)
  //boolean
  //true

  f.call({},1,2)
  //object
  //{}

  f.call(function(){},1,2)
  //function
  //ƒ (){}

  f.call([1,2],1,2)
  //object
  //(2) [1, 2] 是一个真数组

参数0为简单类型的值时,this就是简单类型数据,且值为参数0
参数0为复杂类型的值时,this就是复杂类型数据,且值为参数0

4 Call Stack/调用栈

Call Stack是调用栈,是计算机科学中存储有关正在运行的子程序的消息的栈

调用栈最经常被用于存放子程序的返回地址。在调用任何子程序时,主程序都必须暂存子程序运行完毕后应该返回到的地址。因此,如果被调用的子程序还要调用其他的子程序,其自身的返回地址就必须存入调用栈,在其自身运行完毕后再行取回。在递归程序中,每一层次递归都必须在调用栈上增加一条地址,因此如果程序出现无限递归(或仅仅是过多的递归层次),调用栈就会产生栈溢出

用一个递归函数来说下在函数执行过程中,栈内存是如何变化的

function sum(n){
    if(n == 1){
        return 1
    }else{
        return n + sum.call(undefined, n-1)
    }
}

sum.call(undefined,5)


从图中可以看到,在代码执行的过程中,每一个出现的子程序都会出现在已有程序的上方。如果一个子程序执行完毕了,那么这个子程序就会从Call Stack中被清除,堆在上面的子程序必定会比下面的子程序先被清除

栈溢出
栈内存是有限的,如果同时存在的子程序过多,超过了栈内存的承受上限,就会发生栈溢出

例如上面的递归代码,如果求sum,call(undefined,100000),由于递归的原因,栈内存从下到上会储存sum,call(undefined,100000)sum,call(undefined,99999)、……、sum,call(undefined,1),但由于栈内存无法将这些全部放下,就会发生栈溢出

5 Scope/作用域

5.1概述

这里我用俗语说下,就是如果有一个层层嵌套的函数,而且在嵌套函数中定义了几个同名变量,那么在调用一个变量时,就需要了解变量的作用域来确定调用的是在哪里定义的变量

var a = 1;
function f1(){
  var a=2;
  f2.call();
  console.log(a);
  function f2(){
    var a=3;
    console.log(a);
  }
}
f1.call();
console.log(a);

其作用域示意图表示为:


上述代码我们从上到下依次来看,看console.log(a)中的a到底指的是在哪里定义的a

  • 第一个console.log(a)
    这个console.log(a)是在f1函数当中的,所以首先在f1函数的范围中寻找,看有没有定义a,如果没有再到它的上一级范围内去寻找

  • 第二个console.log(a) 这个console.log(a)是在f2函数当中的,所以首先在f2函数的范围中寻找;如果f2中没有定义a,则去f1中去寻找,如果还没有定义a,则再往上一级范围去寻找

  • 第三个console.log(a)
    这个console.log(a)是在全局函数当中的,所以直接在全局函数中寻找
    个人总结:

  • 变量的作用域为当前的函数空间与其包含的所有子函数

  • 如果一个变量被重复定义,在一个具体函数中,当前函数定义变量的优先级高于父级函数定义的变量
    在判断所调用的变量是在哪里定义时,要注意变量声明提升

5.2 变量声明提升

在JavaScript中,有一个特性叫做变量声明提升;简单的说,就是不管你在哪里声明一个变量,JavaScript总将声明提升到当前区域的头部

console.log(a); // undefined
var a = 1;

按照我们的思想,在打印a的时候,a根本就没声明,那应该会报错Uncaught ReferenceError: a is not defined,而实际上返回的是undefined,这就是变量声明提升。JavaScript将声明语句提到了头部,但赋值语句并没提升,所以a只声明未赋值,得到undefined

其实际代码相当于:

var a;
console.log(a); // undefined
a = 1;

5.3 几个例子

  • 第一个
var a = 1;
function f1(){
    console.log(a); // undefined
    var a = 2;
}
f1.call();

在这里,打印a时,现在f1函数中寻找a,由于变量声明提升,其实在console.log(a)之前是声明了a的,只是未赋值,这样就不用到上级全局函数中去寻找a

  • 第二个
var a = 1;
function f1(){
    var a = 2;
    f2.call();
}
function f2(){
    console.log(a); // 1
}
f1.call();

这里很容易将console.log(a)想成是打印f1函数中的那个a,其实不然,虽然f1调用了f2,但是f2是处于全局范围内的,它在f1之外,所以实际引用的是全局范围定义的a

  • 第三个
var liTags = document.querySelectorAll('li')
for(var i = 0; i<liTags.length; i++){
    liTags[i].onclick = function(){
        console.log(i) // 点击第3个 li 时,打印 6
    }
}

点击第3个li时,打印 2 还是打印 6?

答案是6,因为代码在运行之后,for循环迅速完成,此时i早已变成了6,所以不管你点第几个li,都会打印6

5.4 小心eval

eval接受字符串为参数,然后将字符串当作程序的代码来执行,就像真的在程序中写了代码
所以这就会导致一些变量会随着eval的执行而被定义,不过这只在普通模式下有效,在严格模式下还是不会定义变量

  • 普通模式
var a = 1;
function f1(){
    eval(s);
    console.log(a); // 3
    var a = 2;
}
var s='var a=3;';
f1.call();

正是由于eval的存在,导致在这里将s的字符串内容执行了,声明并定义了a = 3;,所以此时console.log(a);打印的是3

  • 严格模式
var a = 1;
function f1(){
    "use strict";
    eval(s);
    console.log(a); // 是多少
    var a = 2;
}
var s='var a=3;';
f1.call();

在严格模式下,执行eval并未声明变量a,所以console.log(a);a是由变量声明提升所声明的a,所以打印的是undefined

6 Closure/闭包

6.1 概念

在MDN上的定义为:闭包是函数和声明该函数的词法环境的组合

function init() {
    var name = "Mozilla"; // name 是一个被 init 创建的局部变量
    function displayName() { // displayName() 是内部函数,一个闭包
      console.log(name); // 使用了父函数中声明的变量
    }
    displayName();
}
init();

其中函数displayName()可以访问到函数之外的name值,而且nameinit()函数生成的局部变量,并不是全局变量

以我自己的理解,闭包就是函数以及这个函数能访问到的其他函数局部变量的集合

6.2 闭包的作用

  • 闭包常用来间接访问一个变量 也就是这个变量没出现在这个函数中,但这个函数可以访问到,相对于这个函数来说,这个变量被隐藏了 就上面的代码来看,函数displayName()并没有任何地方声明了name,但它就是可以访问得到这个变量

  • 让变量的值始终保存在内存中 由于垃圾回收机制,如果一个函数在调用完之后,就会被回收,下次再调用时函数声明的变量又是崭新的状态。而闭包的使用让函数声明的变量值一直存在内存中

function f1(){
  var n=1;
  function f2(){
    n++;
    console.log(n);
  }
  return f2;
}
var r=f1();
r() // 2
r() // 3

以上代码按“直觉”来说,在调用一次函数之后,函数就会被回收,下一次再调用时,又是新的状态,但是两次调用打印出来的n并不相同,这也就是说,n的值被保存了,并未被垃圾回收机制清除

从代码中,r其实就是闭包函数f2f1f2的父函数,而f2被赋值给了r这个全局变量,导致f2一直在内存中,并未被回收,而f2是依赖f1存在的,这就导致f1f2一直存在内存之中,并未被回收

6.3 使用闭包的注意点

  • 由于闭包会使得函数的变量一直在内存中,所以不能滥用闭包,会造成性能问题
    解决的办法是在退出函数之前,将不用的局部变量清除

  • 闭包会在父函数外部,改变父函数内部变量的值。所以如果你把父函数当作对象使用,把闭包当作它的公用方法,把内部变量当作它的私有属性,这时一定要小心,不要随便改变父函数内部变量的值


目前了解的还不够多,如有错误之处,欢迎指出