有几天没有更新JS灵魂之问的专栏系列文章了,秋招季,也在忙着备战笔试面试。今天得空再来写一篇文章,本篇要讲解的内容是关于 函数基础
,那今天这篇看能不能问倒你了,一起来探索一下吧。
仰望星空的人,不应该被嘲笑
开门见山,下面代码会输出什么,不知道小伙伴平常有咩有想过这类问题
var test = function test1() {
var a = 1;
console.log(a);
}
test()
test1()
答案是 1
和 报错
,报错信息如下:
这是为什么呢?实际上,我们申明了一个变量 test
,把这个函数 test1
赋给它的时候,就已经把这个变量赋予了函数的功能,调用 test()
方法当然能够正常运行,输出 1
。而对于表达式赋值,会自动忽略后面的函数名称,也就是说写与不写并不影响 test()
方法的执行。不信,看看下面代码会输出什么?
var test = function() {
var a = 1;
console.log(a);
}
test() // 1
那这样,是不是说 test1
完全没作用了,那写着干嘛,还多几个字符。当然不是!,test1
在函数体内部是可见的,而在外部却不可见,通过这样,我们就可以实现递归操作
var test = function() {
var a = 1;
console.log(a);
}
而对于上述代码,后面函数体没有名字,我们称之为 匿名函数,是不是有点印象了,原来就是这东西,哟西~
而通过这种方式赋值的表达式,我们称之为 匿名函数表达式,也称为 函数字面量,这些专有名词一出来,瞬间觉得有点逼格了有没有!
字面量这种东西,简单来说就是数据,例如下述,有数字字面量,字符串字面量,数组字面量
等等。简单理解就是,对于赋值的过程,右边的数据就是字面量。
var a = 10
var b = '111'
var c = [1,2,3]
补充:如何获取形参和实参对应的长度?
function test (a,b) {
console.log(test.length) // 形参的长度 2
console.log(arguments.length) // 实参的长度 3
}
test(1,2,3)
我想小伙伴们应该清楚实参和形参是什么玩意,但是我们可以更改实参的值吗?例如下述代码,会输出什么呢?
function test(a, b) {
a =3;
console.log(arguments[0]);
}
test(1,2);
答案是 3
,我们可以修改实参的值。
刚刚那题只是简单热个身,继续下一题吧,我们可以改变 b
的值吗?(提示:注意我并没有传对应实参哦~)
function test(a, b) {
b = 3;
console.log(arguments[1]);
}
test(1);
答案是 undefined
,因此对于上一题表述,要修改一下:对于实参传递过来确定的值,我们是可以进行修改的,而如果实参并没有传递值过来,我们是不能进行修改的。这就是形参和实参的映射关系。
简单解释一下形参和实参的映射关系,其实实参和形参不能说是一类的,看上述代码,我们可以通过 arguments
来获取我们的实参,可以看做是一个数组里面的某一项值,而数组是存放堆内存的,而对应我们形参其实是存放在栈内存的,它们之间会有一个映射关系,并且是一对一对应的,上述我们实参没有对b
进行赋值,尽管修改了形参,但改变不了我们的 arguments[1]
就是这个道理。(没有建立一对一映射关系)。
再来一道引申题,为后续内容做铺垫。下面 a b c
分别会输出什么?
a = 1;
function test1 () {
var b = 2;
console.log(a) //
function test2() {
var c = 3
console.log(b); //
}
test2();
console.log(c); //
}
test1();
答案是 1 2 报错
,这就牵扯到 scope
问题了,简单理解就是函数内部能访问外面的变量,而函数外面却不能访问内部的变量,也就是闭包问题。(这个后文会提到)
如果实参没有赋值,那么形参怎样设置默认参数呢?说到默认参数,我想你应该会想到如下代码:
function test(a = 1, b = 1) {
console.log(a)
console.log(b)
}
test() // 1 1
好的,上述问题算是开胃小菜,我们继续,我如果给 a
设定默认值,而 b
通过实参传递过来呢?可以实现吗?之前没有传参的话,不是默认打印 undefined
吗,那我现在给 a
传递一个 undefined
,是不是就会定为默认值。
function test(a = 1, b) {
console.log(a)
console.log(b)
}
test(undefined, 2)
答案是可以的,上述代码输出结果为 1 2
。简单解释一下,在之前我们将了形参实参是有一个映射关系,对于堆内存 arguments
里面,如果给了 undefined
,那么就会去栈内存形参里面找,如果不为 undefined
,则会设置形参的默认值。(其实这是 es6
的语法了)
那么,可以用es5
的方式实现一下吗?(当然可以,见代码)
function test(a, b) {
a = arguments[0] || 1
b = arguments[1] || 1
console.log(a)
console.log(b)
}
test(undefined, 2)
预编译总结一下就是如下几点:
下面这两段代码,熟悉的同学一下就明白了,面试常考的经典题!
test()
function test(){
console.log(1)
}
console.log(a);
var a = 1;
这就扯到了函数声明提升和变量提升相关的问题了。这里总结整理一下:
函数声明会进行整体的提升,而变量只有声明进行了提升,赋值不会提升
关于变量那块,举例下面代码,其实是有两个步骤:第一,进行变量声明 var a;
第二,进行赋值操作,a = 1;
var a = 1;
好了,这里我就认为你已经理解了提升相关的知识了,我们来看一道题吧:
console.log(a)
function a(a) {
var a = 10;
var a = function () {
}
}
var a = 1;
答案是[Function: a]
(即函数 a
),这里可能一下也想不明白,我们先来讲一下知识,再来解决这个问题吧。
讲解暗示全局变量 imply golbal variable
下面代码会输出 1
,比较简单,就直接说答案了。实际上这里就暗示全局变量了,因为全局有一个 window
对象 ,下面代码也可以这样表示 window.a = 1
,所有权归 window
。
a = 1;
console.log(a);
这又让我想到了下述代码,b
能打印出来吗?还是会报错?
function test() {
var a = b = 1;
}
test();
console.log(b)
答案是 1
,能打印出来 b
,这种写法就是经典的变量泄露问题。而 a
没办法打印,因为它是test
函数的局部变量,相当于闭包内的变量,外层没办法访问闭包内的变量。
继续,这次加大一点难度。下面代码又分别输出什么呢?
function test(a){
console.log(a);
var a = 1;
console.log(a);
function a() {
}
console.log(a);
var b = function(){
}
console.log(b)
function d(){
}
}
test(2);
放答案之前,先总结一下知识点,函数在执行之前,会生成一个 AO
(activation object
,也称为活动对象或者函数上下文)这个AO
会按照如下形式创建:
下述代码是预编译(即函数执行之前AO
对象的结果):
AO = {
a: undefined -> 2 -> function a() ()
b: undefined
d: function d() {
}
}
下述代码是执行函数之后AO
对象的结果:
AO = {
a: undefined -> 2 -> function a() () -> 1
b: undefined -> function() {
}
d: function d() {
}
}
直接看着上述执行完后的AO
,对于第一个输出,由与在赋值 a
为 1
之前,所以我们直接打印 Function: a
,对于第二个输出,对 a
变量进行了赋值为 1
的操作,所以取 AO
对象中的最后一个 1
,打印 1,对于第三个输出,没有其它赋值操作了,直接输出 1 ,对于 b
,输出 Function: b
,这个不需要太多解释。
最后,给出例题的答案:
[Function: a]
1
1
[Function: b]
下面来一道例题,巩固一下,小伙伴们可以自己拿过去做一遍。
function test(a,b) {
console.log(a); // 1
c = 0;
var c;
a = 5;
b = 6;
console.log(b); // 6
function b(){
}
function d(){
}
console.log(b) // 6
}
test(1)
/*
AO = {
a: undefined -> 1 -> 5
b: undefined -> function b(){} -> 6
c: undefined -> 0
d: function d(){}
}
*/
接下来,换类型了哈,下面会输出什么呢?
var a = 1;
function a () {
console.log(2)
}
console.log(a)
答案是 1
,不用过多解释。
举上述例子是为了引出下文,因为这已经不是函数内部的问题了,是全局的问题了。JS
执行之前,会产生一个叫做 GO
的东西,也称为 global object
(全局上下文),也会按照如下步骤进行创建:
GO = {
a: undefined -> function a() {
} -> 1
}
所以,答案为 1
。实际 GO
,就是 window
,window
存储过程就是这样的!
好的,继续下一题,又会输出什么?
console.log(a,b)
function a() {
}
var b = function() {
}
答案是 [Function: a] undefined
,这个答案也许和你想的完全相反,有没有?按照 GO
来解决这道题吧!
GO = {
a: undefined -> function() {
}
b: undefined
}
这是因为在 console.log(a,b)
在 var b = function() {}
之前,此时还没有对 b
变量进行赋值,而 a
通过函数声明整体提升,会输出 funtion a() {}
,如果上述代码改成如下代码 b
变量就会有对应值啦。
var b = function() {
}
console.log(a,b)
function a() {
}
回到我们开头的一道题,现在解决它应该没问题了,会输出什么呢?
console.log(a)
function a(a) {
var a = 10;
var a = function () {
}
}
var a = 1;
由于并没有执行函数,我们不用 AO
,改用 GO
,这道题其实和上一道题差不多,因为我们 console.log(a)
在赋值操作之前,因此我们不会有 1
的结果,所以我们会打印 function a(){}
GO = {
a: undefined -> function a(){
}
}
加大难度,看这一题,结合上述所说 AO
和 GO
来做,试一试!
var b = 3;
console.log(a);
function a(a){
console.log(a);
var a = 2;
console.log(a);
function a(){
var b = 5;
console.log(b);
}
}
a(1);
答案:
[Function: a]
[Function: a]
2
5
我觉得能把这道题完整做出来, AO
和 GO
基本没啥问题了,现在来解释一波:
直接上 GO
、AO
,后续不再过多解释了,如果还有不明白的小伙伴,建议往上再复习一遍,相信可以独立解决这道题的 (*^▽^*)
GO = {
b: undefined -> 3
a: undefined -> function a() {
}
}
AO = {
a: undefined -> 1 -> function a(){
} -> 2
b: undefined -> 5
}
不知道上一道题做的咋样,这道题我们找一点自信,看看会输出什么?
a = 1;
function test() {
console.log(a);
a = 2;
console.log(a);
var a =3;
console.log(a);
}
test();
var a;
答案是 undefined 2 3
,还是老规矩,直接上 GO
和 AO
GO = {
a: undefined -> 1
test: function(){
}
}
AO = {
a: undefined -> 2 -> 3
}
此时,可能就有小部分长的比较帅的小伙伴就要问了,为啥我第一个 a
不是打印 1
呢,GO
里面不是可以取嘛? 确实,但是你漏掉了前提, AO
里面此时存在变量 a
,就不会去GO
里面找了,这里容易掉坑,必须注意!
function test(){
console.log(b);
if(a){
var b = 2;
}
c = 3;
console.log(c);
}
var a;
test();
a = 1;
console.log(a);
答案: undefined 3 1
,直接上 GO
和 AO
,解释一个地方,为啥 b
要放在 AO
里面,因为 js
在预编译时,不会管你什么条件执不执行,只看你是否进行了申明,简单来说,只要在函数内声明了,那么我们就放入 AO
里面,否则放在全局的GO
里面。
GO = {
a: undefined -> 1
test: fucntion(){
...}
c: undefined -> 3
}
AO = {
b: undefined
}
最后,我们来几道题找点自信,结束这让人费解的 GO
和 AO
吧,答案里我就只给 GO
和 AO
啦,还不会方法的,建议多看上文内容,我就不作解释了。
function test() {
return a;
a = 1;
function a() {
}
var a = 2;
}
console.log(test());
答案: [Function: a]
GO = {
test: function() {
...}
}
AO = {
a: undefined -> function a(){
} -> 1 -> 2
}
继续,下一题:
function test() {
a = 1;
function a() {
}
var a = 2;
return a;
}
console.log(test())
答案: 2
GO = {
test: function(){
...}
}
AO = {
a: undefined -> function a(){
} -> 1 -> 2
}
加大难度,注意头发…
a = 1;
function test(e){
function e(){
}
arguments[0] = 2;
console.log(e);
if(a){
var b = 3;
}
var c;
a = 4;
var a;
console.log(b);
f = 5;
console.log(c);
console.log(a);
}
var a;
test(1);
console.log(a);
console.log(f);
答案是 2 undefined undefined 4 1 5
GO = {
a: undefined -> 1
test: function(){
...}
f: undefined -> 5
}
AO = {
e: undefined -> 1 -> function e(){
} -> 2
b: undefined
a: undefined -> 4
c: undefined
}
下面来一点加餐,我想小伙伴们一定是不满足于上述 “简单” 的输出题的,下面来几道经典笔试题:
第一题,下面会输出什么?
var a = false + 1;
console.log(a);
答案是 1
,存在隐式类型转换。
var b = false == 1;
console.log(b)
答案是 false
,存在隐式类型转换。
可能前两道题比较简单,看看下面这份代码吧:
if(typeof(a) && (-true) + (+undefined) + ''){
console.log(1);
}else{
console.log(0);
}
答案是 1
,也是存在隐式类型转换,并且结合了上文的知识,一道非常不错的题。
console.log(-true) // -1
console.log(+undefined) // NaN
if(typeof(a) && (-true) + (+undefined) + ''){
console.log(1);
}else{
console.log(0);
}
console.log(typeof(-'123')) // number
console.log((-true) + (+undefined) + '') // NaN
上面那道题解决了的话,下面这道题就是小试牛刀了,看看会输出什么?
if(1+5*'3' === 16){
console.log(1);
}else{
console.log(0);
}
答案是 1
,虽然 ===
不能进行隐式转换,但是没说左边或右边单独不能进行隐式转换呐。
继续,
console.log(!!' ' + !!'' - !!false || '未通过');
答案:1
,因为左边 1+0-0
得到 0
,所以不会走右边。
window.a || (window.a = '1')
console.log(window.a)
答案:1
,因为这里有 ()
括号,优先级最高,首先会对 window.a
赋值 1
,然后判断左边 window.a
为真,直接走下面输出函数。 而如果说先走左边window.a
判断为 false
,然后走右边进行赋值为 1
,最后打印 1
,这样回答的话就错啦,没有考虑括号优先级。
文章产出不易,还望各位小伙伴们支持一波!
往期精选:
小狮子前端の笔记仓库
访问超逸の博客,方便小伙伴阅读玩耍~
学如逆水行舟,不进则退