之前学习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,它其实是一个存在在栈中的变量,这个变量引用了一个函数对象。
也就是说,在声明函数的时候,内部其实是先创建一个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,这没什么疑问,主要关注点在于参数fn
和var 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了;
经过 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的数据类型,之前的函数引用被断开。
一部一部,其实很简单。