【亡羊补牢】JS灵魂之问 第15期 修炼内功 让人费解的 GO 和 AO,一大串输出题,你接得住吗?

引言

有几天没有更新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] 就是这个道理。(没有建立一对一映射关系)。

让人费解的 GO 和 AO

作用域引入

再来一道引申题,为后续内容做铺垫。下面 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);

放答案之前,先总结一下知识点,函数在执行之前,会生成一个 AOactivation 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,对于第一个输出,由与在赋值 a1 之前,所以我们直接打印 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,就是 windowwindow存储过程就是这样的!

好的,继续下一题,又会输出什么?

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(){}
}

加大难度,看这一题,结合上述所说 AOGO 来做,试一试!

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

我觉得能把这道题完整做出来, AOGO 基本没啥问题了,现在来解释一波:

直接上 GOAO,后续不再过多解释了,如果还有不明白的小伙伴,建议往上再复习一遍,相信可以独立解决这道题的 (*^▽^*)

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,还是老规矩,直接上 GOAO

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,直接上 GOAO,解释一个地方,为啥 b 要放在 AO 里面,因为 js在预编译时,不会管你什么条件执不执行,只看你是否进行了申明,简单来说,只要在函数内声明了,那么我们就放入 AO里面,否则放在全局的GO里面。

GO = {
  a: undefined -> 1
  test: fucntion(){...}
  c: undefined -> 3
}
AO = {
  b: undefined
}

最后,我们来几道题找点自信,结束这让人费解的 GOAO吧,答案里我就只给 GOAO啦,还不会方法的,建议多看上文内容,我就不作解释了。

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,这样回答的话就错啦,没有考虑括号优先级

最后

文章产出不易,还望各位小伙伴们支持一波!

往期精选:

小狮子前端の笔记仓库

访问超逸の博客,方便小伙伴阅读玩耍~

学如逆水行舟,不进则退
一百个Chocolate CSDN认证博客专家 CSDN博客专家 博客之星 前端开发攻城狮
JS,TS,LeetCode,Vue,React,算法爱好者。
主要分享前端知识,立志成为优秀前端博主。
座右铭:学如逆水行舟,不进则退!
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页
实付 9.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值