Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
yyzclyang opened this issue May 16, 2018 · 0 comments
Open

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

yyzclyang opened this issue May 16, 2018 · 0 comments

Comments

@yyzclyang
Copy link
Owner

yyzclyang commented May 16, 2018

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); //等价于
  变量.函数名.call(变量,参数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 使用闭包的注意点

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

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

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

@yyzclyang yyzclyang changed the title JavaScript的函数(function) 2018.05.17 函数的this、Call Stack、作用域初体验 May 17, 2018
@yyzclyang yyzclyang changed the title 2018.05.17 函数的this、Call Stack、作用域初体验 函数的this、Call Stack、作用域初体验 Aug 13, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant