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-value、length,但是其__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]
当参数0为undefined或者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值,而且name是init()函数生成的局部变量,并不是全局变量
以我自己的理解,闭包就是函数以及这个函数能访问到的其他函数局部变量的集合
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其实就是闭包函数f2,f1是f2的父函数,而f2被赋值给了r这个全局变量,导致f2一直在内存中,并未被回收,而f2是依赖f1存在的,这就导致f1和f2一直存在内存之中,并未被回收
6.3 使用闭包的注意点
-
由于闭包会使得函数的变量一直在内存中,所以不能滥用闭包,会造成性能问题
解决的办法是在退出函数之前,将不用的局部变量清除 -
闭包会在父函数外部,改变父函数内部变量的值。所以如果你把父函数当作对象使用,把闭包当作它的公用方法,把内部变量当作它的私有属性,这时一定要小心,不要随便改变父函数内部变量的值
目前了解的还不够多,如有错误之处,欢迎指出