关于JS函数声明前置的笔记

之前学习JS的变量声明前置和函数声明的相关内容时,忘记讨论一个互相影响的问题了,这里特地补充一下。

即JavaScript函数声明和变量声明同名情况下的优先级讨论。


首先看一个栗子

var fn = 10;
function fn() {}
console.log(fn);    // 10

function fn2() {}
var fn2 = 10;
console.log(fn);    // 10

无论是哪种书写方式,最后的console值总为10,即,最后一个生效的是和变量声明的代码相关。

我们改写一下:

// 栗子1
console.log(fn);    // funciton fn() {...}
var fn = 10;
console.log(fn);    // 10
function fn() {}
console.log(fn);    // 10

// 栗子2
console.log(fn2);    // funciton fn2() {...}
function fn2() {}
console.log(fn2);    // function fn2() {...}
var fn2 = 20;
console.log(fn2);    // 20

// 栗子3
console.log(fn3);    // funciton fn3() {...}
var fn3;
console.log(fn3);    // funciton fn3() {...}
function fn3() {}
console.log(fn3);    // funciton fn3() {...}

这里可以肯定的是,函数声明优先级的确大于变量声明

对于栗子1,第二个console之前存在一个同名的变量声明,这里就将fn函数cover掉了,我们知道,变量会声明前置但是赋值不会,这里的var fn = 10实则其实只进行了a = 10的操作,对,没错,你没看错,只有赋值没有声明,为什么?因为声明早已在函数声明时就进行了!


这里要补充一个知识点,以function fn() {}为例,我们看见的函数名:fn,它其实是一个存在在栈中的变量,这个变量引用了一个函数对象。

关于JS函数声明前置的笔记_第1张图片
截取自JavaScript编程全解

关于JS函数声明前置的笔记_第2张图片
截取自JavaScript编程全解

也就是说,在声明函数的时候,内部其实是先创建一个Function类的实例,而函数名作为一个变量去引用这个函数对象。

这其实也就是函数就是对象的正确理解。

这在栗子3中也可以看到,在栗子3中,首先function fn3() {}这里,函数声明前置,其实就是声明一个变量,名为fn3,引用了一个函数对象。而后,我们重新对var fn3;进行了一次声明操作,发现并没有任何变化,fn3依旧持有一个函数对象的引用。而不是想当然的undefined


我们在再看栗子2,栗子2和栗子1是作为对照的。

// 栗子1
console.log(fn);    // funciton fn() {...}
var fn = 10;
console.log(fn);    // 10
function fn() {}
console.log(fn);    // 10

// 栗子2
console.log(fn2);    // funciton fn2() {...}
function fn2() {}
console.log(fn2);    // function fn2() {...}
var fn2 = 20;
console.log(fn2);    // 20

主要变化在于一个是函数声明和变量声明的顺序。
其实只要记住一句话,赋值操作不会前置,这里就能理解了。

  • 栗子1的第二个console.log是在赋值操作之后的,一旦进行var fn = 10;(其实是fn = 10),fn的数据类型就是一个数值了。
  • 而栗子2的第二个console.log是在赋值操作之前,数据类型并没有改变,所以是一个函数。

我们就来几个test:

function fn(fn) {
    console.log(fn);

    var fn = 3;
    console.log(fn);
}

fn(10);

// 10
// 3

因为这一问题有些特殊,我直接就附上答案。
最后一个console是3,这没什么疑问,主要关注点在于参数fnvar fn = 3;之间发生了什么?,实参与内部变量声明的关系是什么?

这里我需要附上JS高程里的一些文字(有些长):

ECMAScript不介意传递进来多少个参数,也不在乎传进来的参数是什么数据类型。即便你定义的函数只接收两个参数,在调用函数时也未必一定要传递两个参数,可以是一个、两个、三个甚至不传参数,而解析器不会有什么怨言。之所以这样,原因是ECMAScript中的参数在内部是用一个数组表示的。函数接受到的始终都是这个数组,而不关心数组中包含哪些参数(如果有的话)。如果这个数组中不包含任何元素,无所谓;如果包含多个元素,也没有问题;在函数体内可以通过arguments对象来访问这个数组,从而获取传递给函数的每一个参数。
其实,arguments对象只是与数组类似,但它并不是Array的实例,因为可以使用括号访问它的每一个元素(arguments[0]、arguments[1]以此类推)。
通过访问argument.length可以获取有多少个参数传递给了函数。

重点1:arguments对象内的值和参数不是一个“东西”,是两个......
重点2:arguments对象的值与参数的值保持同步,更改arguments对象内的值会影响参数的值。

栗子1

function doAdd(num1,num2) {
    arguments[1] = 10;
    console.log(arguments[0] + num2);
}

doAdd(0,100);
// 10

栗子2

function fn(a) {
    console.log(arguments.length);      // 3
    console.log(arguments[0]);          // 1
    console.log(a);                     // 1
    var a = 100;
    arguments[0] = 1000;
    console.log(arguments[0]);          // 1000
    console.log(a);                     // 1000
    
 }
fn(1,2,3);
  • arguments对象里有几个元素,不是函数定义的括号里的形参决定的,而是,在调用函数时,你传入几个参数,argument对象内就有几个元素,顺序和传参时相同;
  • function doAdd(num1,num2,num3) {} doAdd();形参定义了三个,但是无传参,那么num1,num2,num3的值就是undefined。arguments.length为0;

正如栗子1,读取arguments[1]的值和读取num2的值,它们并不是访问相同的内存空间,它们的内存空间是独立的。

严格模式下,arguments对象的值和形参的值是独立的
function test(num1,num2){
'use strict';
console.log(num1,arguments[0]);//1 1
arguments[0] = 2;
console.log(num1,arguments[0]);//1 2
num1 = 10;
console.log(num1,arguments[0]);//10 2
}
test(1);

好,回到那个test。

function fn(fn) {
    console.log(fn);

    var fn = 3;
    console.log(fn);
}

fn(10);

// 10
// 3

我的想法是:
参数fn是个什么东西?它肯定不是argument[0],但内部肯定有一个值传递的过程,因为最初的10保存在arguments[0]上。

是变量吗,如果是变量,那么有没有变量声明这一过程?即从undefined到10的过程?
这里我用chrome develop tools看了看,发现当fn进栈的时候,fn就已经为10了;


关于JS函数声明前置的笔记_第3张图片
关于JS函数声明前置的笔记_第4张图片

经过 var fn = 3;fn被重新赋值为3。

结论:

函数的参数是在函数内部可用的,是局部变量

javascript函数参数的作用域于参数类型无关,也就是说不管参数是函数还是其他类型,这个参数的作用域只在接收这个参数的函数内有效,所以说他是个局部变量,只可以在这个方法内部使用


来看下一题:

console.log(a);  
a();           
var a = 3; 
function a() {
    console.log(10);  
}
console.log(a);  
a = 6;        
a();          

分析:
首先函数声明前置,但是!由于赋值操作没到,所以此时的a依旧引用了一个函数。

其实声明操作早已在function a() {...}这里就进行了,整个代码只有一次声明,发生在函数声明处(优先),所以没有发生变量声明前置就没有这一操作。

所以等于就是console了一个函数。
console.log(a); // 第一个弹出:function a() {...} 函数语句

之后调用a函数,a函数进执行栈,执行函数体内部代码,则:
console.log(10); // 第二个弹出:10

这里就到了赋值操作:
a = 3;
到这里,a就不在是一个引用对象了,而是保存一个数值。那么继续执行。
console.log(a); // 第三个弹出:3

然后又进行一次赋值操作:
a = 6;
此时变量a的值为6。

那么,a已经不是函数了,调用a()会发生啥?
当然报错啦233333。
TypeError: a is not a function


改写一下:

console.log(a);         // function a() {...}
var a = 3;              // a = 3
console.log(a);         // 3
a();                    // a is not function
function a() {
    console.log(10);  
}
console.log(a);
a = 6;               
a();               

最后引用一篇文章做总结:

bug达——JS中变量名和函数名重名
1. 函数声明会置顶
2. 变量声明也会置顶
3. 函数声明比变量声明更置顶:(函数在变量上面)
4. 变量和赋值语句一起书写,在js引擎解析时,会将其拆成声明和赋值2部分,声明置顶,赋值保留在原来位置
5. 声明过的变量不会重复声明

面试时,务必看清楚,如果同时存在重名函数声明和变量声明,那么实际操作中,变量名只声明了一次,引用了一个函数。
如果后面有赋值操作,根据右值更改变量a的数据类型,之前的函数引用被断开。
一部一部,其实很简单。

你可能感兴趣的:(关于JS函数声明前置的笔记)