函数和作用域
详情参考饥人谷课件,转载请务必注明来源,谢谢
JavaScript函数是指一个特定代码块,可能包含多条语句,可以通过名字来供其它语句调用以执行函数包含的代码语句。
比如我们有一个特定的功能需要三条语句实现
statement1;
statement2;
statement3;
那么每次想实现这个功能的时候就需要写这三句话,很麻烦,我们可以把这三条语句打包为一个函数
function doSomething(){
statement1;
statement2;
statement3;
}
这样每次想实现功能的时候我们就调用一下函数就可以了,调用函数通过函数名称()的形式调用
doSomething();
声明函数
函数声明
function printName(){
console.log('饥人谷')
}
printName()
函数表达式
var printName = function(){
console.log('饥人谷')
}
printName()
注意:函数声明和韩式表达式的写法区别
参数
function printName(name){
console.log(name)
}
printName('hunger')
printName('valley)
函数在定义的时候可以写多个参数,例如
function printPersonInfo(name, age, sex){
console.log(name)
console.log(age)
console.log(sex)
}
arguments
在函数内部,你可以使用arguments对象获取到该函数的所有传入参数,主要使用下标来表示
function printPersonInfo(name, age, sex){
console.log(name)
console.log(age)
console.log(sex)
console.log(arguments)
console.log(arguments[0])
console.log(arguments.length)
console.log(arguments[1] === age)
}
printPersonInfo('饥人谷', 3, 'male')
执行结果
重载
重载是很多面向对象语言实现多态的手段之一,在静态语言中确定一个函数的手段是靠方法签名——函数名+参数列表,也就是说相同名字的函数参数个数不同或者顺序不同都被认为是不同的函数,称为函数重载
在JavaScript中没有函数重载的概念,函数通过名字确定唯一性,参数不同也被认为是相同的函数,后面的覆盖前面的,这是不是意味着JavaScript不能通过重载功能实现一个函数,参数不同功能不同呢?
在JavaScript中,函数调用没必要把所有参数都传入,只要你函数体内做好处理就行,但前提是传的参数永远被当做前几个
function printPeopleInfo(name, age, sex){
if(name){
console.log(name);
}
if(age){
console.log(age);
}
if(sex){
console.log(sex);
}
}
printPeopleInfo('hunger', 3);
printPeopleInfo('hunger', 3, 'male');
返回值
有时候我们希望在函数执行后给我们一个反馈,就像表达式一样,给我们个结果,我们可以通过return来实现
function fn(a, b){
a++;
b++;
return a + b;
}
var result = fn(2, 3);
console.log(result);
这样我们就能拿到函数希望给我的反馈了,调用return后,函数立即中断并返回结果,即使后面还有语句也不再执行
其实我们不写return语句,函数也会默认给我们返回undefined
递归
简单来说就是自己调用自己,递归需要设置结束条件(以下为n!的递归)
function factorial(n){
if(n === 1){
return 1
}
return n * factorial(n-1)
}
factorial(3) //6
声明提前
和变量的声明会前置一样,函数声明同样会前置,如果我们使用函数表达式,那么规则和变量一样
console.log(fn) //undefined
var fn = function(){}
如果我们使用函数声明的方式,那么即使函数写在最后也可以在前面语句调用
fn() // "1"
function fn(){
console.log('1')
}
命名冲突
当在同一个作用域内定义了名字相同的变量和方法的话,会根据前置顺序产生覆盖
var fn = 3;
function fn(){}
console.log(fn); // 3
相当于
var fn
function fn(){} //覆盖上面的
console.log(fn) //ƒ fn(){}
fn = 3 //重新赋值
console.log(fn) //3
当函数执行有命名冲突的时候,可以认为在还是内部一开始有隐藏的声明变量这个操作
function fn(fn){
console.log(fn);//10
var fn = 3;
console.log(fn);//此时覆盖掉原来的fn,输出为3
}
fn(10) //10 3
执行时等价于
function fn(){
var fn
fn = 10
console.log(fn)
fn = 3
console.log(fn);
}
作用域链
题目1
var a = 1
function fn1(){
function fn2(){
console.log(a)
}
function fn3(){
var a = 4
fn2()
}
var a = 2
return fn3
}
var fn = fn1()
fn() //输出多少
解析:
最终结果是要输出fn=fn3,函数执行到fn3再到fn2,然而在fn2里并不能找到a的值,所以往fn2,fn3的上一级去寻找a的值(fn1包含了fn2和fn3),即fn1,此时找到了a的值,a=2,所以最终输出为2
题目2
var a = 1
function fn1(){
function fn3(){
var a = 4
fn2()
}
var a = 2
return fn3
}
function fn2(){
console.log(a)
}
var fn = fn1()
fn() //输出多少
解析:
最终结果是要输出fn=fn3,函数执行到fn3再到fn2,然而在fn2里并不能找到a的值,所以往fn2的上一级去寻找a的值,此时fn2的上一级为fn1,也找不到值,所以只能找全局标量a的值,即var a = 1,此时找到了a的值,a=1,所以最终输出为1
题目3
var a = 1
function fn1(){
function fn3(){
function fn2(){
console.log(a)
}
var a
fn2()
a = 4
}
var a = 2
return fn3
}
var fn = fn1()
fn() //输出多少
解析:
此处涉及一个叫预执行的操作,详情请参考
JS的预解析与执行过程详解
JS的预编译和执行顺序 详析(及全局与局部变量)
最终结果是要输出fn=fn3,函数执行到fn3再到fn2,然而在fn2里并不能找到a的值,所以往fn2的上一级去寻找a的值,此时fn2的上一级为fn3,而此时就涉及到预执行的操作,JS会默认运行操作是从上而下,首先找到的是var a,而此时的a的值已经被赋予undefined,所以最终输出为undefined。
对比以下代码
var a = 1
function fn1(){
function fn3(){
var a = 4
function fn2(){
console.log(a)//4
}
var a
fn2()
a = 5
}
var a = 2
return fn3
}
var fn = fn1()
fn() //输出多少
解析:
他们的唯一不同就是在fn()3里面加了一句var a =4,此时预编译的时候,a的值已经被赋予了4,console.log(a)的值也就确定的,预编译的执行顺序是默认运行操作是从上而下,那后面不管你给a的值赋值多少都不会发生改变
方法总结
函数在执行的过程中,先从自己内部找变量
如果找不到,再从创建当前函数所在的作用域去找, 以此往上
注意找的是变量的当前的状态
作用域链总结
当代码在一个环境中执行时,都会创建一个作用域链。 作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。整个作用域链的本质是一个指向变量对象的指针列表。作用域链的最前端,始终是当前正在执行的代码所在环境的变量对象。
如果这个环境是函数,则将其活动对象(activation object)作为变量对象。活动对象在最开始时只包含一个变量,就是函数内部的arguments对象。作用域链中的下一个变量对象来自该函数的包含环境,而再下一个变量对象来自再下一个包含环境。这样,一直延续到全局执行环境,全局执行环境的变量对象始终是作用域链中的最后一个对象