这篇文章需要重点记忆,面试的过程中经常会涉及到此篇文章中的问题。
一、函数的五种声明方式
1) 具名函数
1 | function fn(参数1,参数2){ |
其中function为关键字,fn为函数名,
2)匿名函数
1 | let x = function(参数1,参数2){ |
使用时直接调用变量:x(1,3);
3)具名函数赋值
1 | let x = function y(arg1,arg2){ |
4)window.Function函数对象
1 | new Function('x','y','return x+y'); |
x,y均为参数,第三个参数为函数体。所有的内容都可以以字符串的方式拼起来,并且可以加入变量,以下:1
2
3let n = 1;
let f = new Function('x','y','return x+'+n+'+y');
f(1,2); // 此时输出4
5)ES6箭头函数
1 | let sum = (x,y) => { |
如果花括号内只有一句话的时候,且返回的不是一个对象时可以这么写:1
2let sum = (x,y) => x+y
sum(1,2); // 返回3
如果箭头函数参数只有一个,写法可以如下:1
2let sum = n => n*n
sum(2); // 返回4
以上。箭头函数是匿名函数,没有名字,需要赋值到一个变量上。
关于函数有一个属性用于获取函数的名字,即name,介绍下每种函数声明方式下获取的name值是什么,只需强制记忆就可以:1
2
3
4
5
6
7
8
9function f1(){}
let f2 = function(){}
let f3 = functionf4(){}
let f5 = new Function('x','y','return x+y');
f1.name; // 输出"f1"
f2.name; // 输出"f2"
f3.name; // 输出"f4"
f5.name; // 输出"anonymous",是匿名的意思
二、如何调用函数
以上,在声明了一个函数之后,会将其参数放置到某个属性中,我们暂时认为是params,函数主体会放置在tbody中,其proto指向Object.prototype,Object.prototype包含一个call()方法,此方法会调用函数体中的tbody。而函数调用就是执行call()方法。
为加深理解,我们直接做出一个函数出来:
1
2
3
4
5
6
7let f = {};
f.params = ['x','y'];
f.fbody = 'console.log("fff")';
f.call = function(){
return window.eval(f.tbody); // eval()将传入的字符串将代码执行,尽量少用
}
f.call() // 输出fff
以上,f是一个函数对象,f.call()是执行这个对象的函数体。
其中f(1,2)和f.call(undefined,1,2)二者执行的效果一致,而call()方法的调用才是函数的真正调用,为硬核,hardcored。第一种方法为语法糖。
函数是一个对象,可以执行代码。
三、this和arguments
1 | f.call(undefined,1,2); |
根据《二、如何调用函数》中的示例,在调用函数的过程中,以上述代码为例,undefined就是this,’1,2’就是arguments。即call的第一个参数可以用this得到,call的后面的参数可以用arguments得到。
我们通过函数调用可以输出下this和arguments:
其中arguments为一个伪数组(长得像数组,但是其proto不是Array.prototype)。
四、call stack调用栈
这个我也说不太明白,直接看例子吧。stack调用栈主要是先进后出。
例子一:普通调用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 function a(){
console.log('a')
return 'a'
}
function b(){
console.log('b')
return 'b'
}
function c(){
console.log('c')
return 'c'
}
a.call()
b.call()
c.call()
前面三个function是函数声明。后面a.call()、b.call()、c.call()是函数调用,这三条语句会依次执行。执行的顺序如上图所示:
首先执行a.call()并放入栈中,紧接着执行console.log(‘a’),此语句执行完成后在栈的队列被销毁然后执行紧接着的语句return ‘a’,这个只返回数据不会在栈中出现,那么接下来就是销毁a.call()。然后执行b.call(),执行顺序跟以上类似。
例子二:嵌套调用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function a(){
console.log('a1')
b.call()
console.log('a2')
return 'a'
}
function b(){
console.log('b1')
c.call()
console.log('b2')
return 'b'
}
function c(){
console.log('c')
return 'c'
}
a.call()
console.log('end')
以上代码在栈中执行的顺序是:
->a.call()
-> a.call() console.log(‘a1’)
-> a.call() ->a.call() b.call()
-> a.call() b.call() console.log(‘b1’)
-> a.call() b.call()
-> a.call() b.call() c.call()
-> a.call() b.call() c.call() console.log(‘c’)
-> a.call() b.call() c.call()
-> a.call() b.call()
-> a.call() b.call() console.log(‘b2’)
-> a.call() b.call()
-> a.call()
-> a.call() console.log(‘a2’)
-> a.call()
-> 空
-> console.log(‘end’)
-> 空(完)
例子三:递归调用1
2
3
4
5
6
7
8function sum(n){
if(n==1){
return 1
}else{
return n + sum.call(undefined, n-1)
}
}
sum.call(undefined,5)
以上代码在栈中执行的顺序是:
1)-> sum.call(undefined,5)
2)-> sum.call(undefined,5) n==1
3)-> sum.call(undefined,5)
4)-> sum.call(undefined,5) n+sum.call(undefined,n-1)
5)-> sum.call(undefined,5)
6)-> sum.call(undefined,5) sum.call(undefined,n-1)
7)-> sum.call(undefined,5) sum.call(undefined,n-1) n-1
8)-> sum.call(undefined,5) sum.call(undefined,n-1)
9)-> sum.call(undefined,5) sum.call(undefined,n-1) n == 1循环值第二步
10)当n==1时,能够返回值了,再一步一步退出栈
四、作用域
1、概念
- 按照语法树,就近原则
- 我们只能确定变量是哪个变量,但是不能确定变量的值
看以下代码及对应图示:1
2
3
4
5
6
7
8
9
10
11
12var 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);
根据上图所示,根据就近原则,在f2()内输出a的值为3,f1()中a的值为2,在全局作用域内a的值为1;
2、常见面试题
面试题一1
2
3
4
5
6var a = 1
function f1(){
alert(a) // 是多少
var a = 2
}
f1.call()
此时输出2,这个地方考的是变量提升。在任何一个函数作用域内,会优先将此作用域下的所有变量提升到最前面,再执行其他语句。所以此时f1()内的代码实际上应该是:”var a = 2;alert(a);”,即此时弹出2。
面试题二1
2
3
4
5
6
7
8
9var a = 1
function f1(){
var a = 2
f2.call()
}
function f2(){
console.log(a) // 是多少
}
f1.call()
根据就近原则,输出1,因为f2()时全局作用域下的方法,其输出的a值应该是全局作用域下的变量a。
面试题三1
2
3
4
5
6var liTags = document.querySelectorAll('li')
for(var i = 0; i<liTags.length; i++){
liTags[i].onclick = function(){
console.log(i) // 点击第3个 li 时,打印 2 还是打印 6?
}
}
输出2。以上语句只是绑定事件,绑定事件结束时循环已经结束,且此时i的值为2。所以无论点击哪个li都会输出2。
五、闭包
如果一个函数,使用了它范围外的变量,那么(这个函数+这个变量)就叫做闭包。举例:
1
2
3
4var a = 1;
functon f(){
console.log(a);
}
详情参考面试题汇总中的Javascript篇第二问。
以上就是一个闭包。其实闭包就是JS函数作用域的副产品。基本上懂函数作用域,闭包也就差不多懂了。这里就先介绍到这里,后续再进行详细介绍。