概念
1、函数声明和函数表达式有什么区别?
ECMAScript规定了三种声明函数方式:
- 构造函数
首先函数也是对象一样,我们可以通过其构造函数,使用new来创建一个函数对象
var printName = new Function("console.log('Byron');");```
这种创建对象的方式按理来说应该是最正规的方式,但是因为性能原因极少使用
- 函数声明
**就像var声明一个变量一样,function声明一个函数**
function functionName(){
statement;
}```
使用function关键字可以声明一个函数,看个例子
function printName(){
console.log('Byron');
}
printName();```
- 函数表达式
函数表达式不是以function开始,而是像声明一个变量一样
var printName = function(){
console.log('Byron');
};```
函数声明和函数表达式比较容易混淆,它们的区别主要有:
- 函数声明总是以function关键字开始,如果不是,那么它就是函数表达式;
- 函数声明最后面一般不写分号,而函数表达式有分号;
- 最重要的一点,对于函数声明,并不仅仅是函数名被提前了,整个函数的定义也被提前了;
而对于函数表达式,规则和声明变量提前一样,只是声明变量被提前,它所赋值的操作并没有被提前。
2、什么是变量的声明前置?什么是函数的声明前置?
-
变量的声明前置
JavaScript引擎的工作方式是,先解析代码,获取所有被声明的变量,给他初始值undefined,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升。
变量提升
上面的效果JS引擎会自动帮我们去这样解析:
默认解析方式 - 函数的声明前置
函数的声明前置包括两种情况,分别为函数声明和函数表达式:-
函数表达式
函数表达式声明前置
上面的代码浏览器会自动帮我们转成下面的方式,效果都是一样的:
JS引擎默认这么做
从上面可以看出函数表达式的声明前置只是将变量的声明前置而已,并不是将整个表达式前置。 -
函数声明
但是对于函数声明前置的则是整个函数声明,即使函数写在最后也可以在前面语句调用:
函数声明的前置
-
最后总结一下:
- 变量声明会提前到作用域的顶部,而赋值部分会留在原来的地方,按次序执行;
- 函数表达式其实和变量是一样的,只是将一个函数赋值给变量,同样也只是这个变量会提前,函数赋值给变量部分依然留在原地;
- 但是对于函数声明就不一样,整个声明都会被前置,但是总是跟随在变量声明的后面,所以即使我们把函数写在最后也可以被前面的语句调用。
3、arguments是什么?
- arguments是一个类数组对象,里面存储的是函数在执行时所传递进来的参数。
- arguments只在函数内部有效,可以在函数内部通过使用arguments对象来获取函数的所有参数;
这个对象为传递给函数的每一个参数建立一个索引,从0开始;
外部传进来的参数还可以通过这个方法被重新赋值; - arguments对象类似于数组,但它并不是真正的数组,除了length,它没有数组所特有的属性和方法;
-
arguments主要用途:arguments对于参数数量是一个可变量的函数来说比较有用。 当我们传递给这个函数的参数数量比它显式声明的参数数量更多的时候,你就可以使用 arguments对象获取到该函数的所有传入参数。
arguments用法
4、函数的重载是怎样实现?
- 重载是很多面向对象语言实现多态的手段之一,在静态语言中相同名字的函数参数个数不同或者顺序不同都被认为是不同的函数,称为函数重载。
- 在JS中没有函数重载的概念,函数通过名字确定唯一性,参数不同也被认为是相同的函数,后面的覆盖前面的。
JS中没有重载 - 那么难道JS中我们就不能通过重载实现一个函数参数不同功能不同吗?这里就可以用到上面的arguments类数组对象来实现了;
用arguments实现重载
5、立即执行函数表达式(IIFE)是什么?有什么作用?
- 立即执行函数表达式(Immediately-Invoked Function Expression)是一种语法,可以在你的函数在定义后立即被执行,这种模式本质上就是函数表达式(命名或者匿名的),在创建后立即执行,其代码格式如下:
//在最前最后加括号
(function atOnce(){
console.log('abc');
}());
//在function外面加括号
(function atOnce(){
console.log('abc');
})();
- 立即执行函数表达式的作用:
- 它可以帮你封装大量的工作而不会在背后遗留任何全局变量;
- 定义的所有变量都会成为立即执行函数的局部变量,所以你不用担心这些临时变量会污染全局空间;
- 这种模式经常被使用在书签工具中,因为书签工具在任何页面上运行并且保持全局命名空间干净非常必要的;
- 这种模式也可以让你将独立的功能封装在自包含模块中;
6、什么是函数的作用域链
- 作用域
作用域就是函数和变量可以访问的范围,JS中变量的作用域分为全局作用域和局部作用域。JS的作用域是靠函数来形成的,也就是说一个函数内定义的变量函数外不可以访问,变量在声明它们的函数体及其子函数内是可见的。这里需要注意一下语句的变量作用域范围。
console.log(j); //undefined
console.log(i); //undefined
for(var i=0;i<10;i++){ //不是函数,是控制语句,所以这里的i和j是全局作用域。
var j = 100;
}
console.log(i); //10
console.log(j); //100
最外层函数和在最外层函数外面定义的变量拥有全局作用域,只有函数才有局部作用域。
for(var i=0;i<10;i++){
var j = 100;
}
function fn(){
var i = 99;
console.log(i); //只有函数才有局部的作用域,先在内部找(找不到再一层层往上面的作用域找),所以输出为99。
}
fn(); // 99
console.log(i); //10,只能取到for里面的10.但是如果上面函数里面的var i = 99;改成i = 99;打印结果为99,所以如果不加var就是全局变量。
当然也不是说不带var的就是全局作用域,再来看个例子。
for(var i=0;i<10;i++){
var j = 100;
}
function fn2(){
console.log(i);
var i = 99; //如果去掉var,结果应该是10,100,100
function fn2(){
i = 100; //没有加var,可以认为这个函数里面变量的作用域在父亲的范围内
}
fn2(); //再执行这个,也就是内部的fn2(),得到100
console.log(i);
}
fn2(); //先执行这个调用,也就是大函数fn2(),得到undefined
console.log(i); //最后执行。这里是全局变量的i,为10
总的来说:
变量没有在函数内声明或者声明的时候没有带var就是全局变量(除了在函数内部定义的子函数情况),拥有全局作用域;
window对象的所有属性拥有全局作用域,在代码任何地方都可以访问;
函数内部声明并且以var修饰的变量就是局部变量,只能在函数体内使用;
函数的参数虽然没有使用var,但仍然是局部变量。
作用域链
代码在执行的过程中,会创建一个作用域链,用来保证执行的环境对变量和函数的有序访问。在函数运行过程中标识符的解析是沿着作用域链一级一级搜索的过程,从当前所在的作用域开始,逐级向上寻找,直到找到同名标识符为止,找到后不再继续遍历,找不到就报错。
JavaScript 开发进阶:理解 JavaScript 作用域和作用域链
代码
1、以下代码输出什么?
function getInfo(name, age, sex){
console.log('name:',name);
console.log('age:', age);
console.log('sex:', sex);
console.log(arguments);
arguments[0] = 'valley';
console.log('name', name);
}
getInfo('hunger', 28, '男');
getInfo('hunger', 28);
getInfo('男');
输出结果分别是:
name: hunger
age: 28
sex: 男
['hunger', 28,'男']
name valley
name: hunger
age: 28
sex: undefined
['hunger', 28]
name valley
name: 男
age: undefined
sex: undefined
['男']
name valley
2、写一个函数,返回参数的平方和
function sumOfSquares(){
//首先这里肯定能想到用arguments
var sumOfSquares=0;
for(var i=0;i<arguments.length;i++){
sumOfSquares=sumOfSquares+arguments[i]*arguments[i]
}
return sumOfSquares; //最后将结果返回给调用函数语句
}
sumOfSquares(2,3,4); // 29
sumOfSquares(1,3); // 10
3、以下代码的输出?为什么
console.log(a);
var a = 1;
console.log(b);
这题代码考察的就是变量声明的提升,相当于以下代码:
var a;
console.log(a); //输出undefined
a=1;
console.log(b); //报错
4、如下代码的输出?为什么
sayName('world'); //输出hello world
sayAge(10); //输出错误,sayAge is not a function
function sayName(name){
console.log('hello ', name);
}
var sayAge = function(age){
console.log(age);
};
这题涉及到函数的声明前置,函数声明和函数表达式的区别,相当于如下代码:
//一般提升的变量放在函数前面,如果变量名和函数名同则函数覆盖变量
var sayAge; //对于函数表达式只是函数变量的声明会提前
function sayName(name){
console.log('hello ', name);
} //对于函数声明,整个都会前置
sayName('world');
sayAge(10);
sayAge = function(age){
console.log(age);
};
5、如下代码的输出?为什么
function fn(){
}
var fn = 3;
console.log(fn); //输出3
这里有两个规则:
- 在同一个作用域中,变量的声明总是会在函数的声明之前,所以对同一名称函数的声明会覆盖变量的声明;
- 如果这个变量有赋值的话,会覆盖方法的声明;
所以转换后的代码为:
var fn; //变量的声明前置,此时fn为普通的变量
function fn(){
} //函数声明覆盖变量的声明,此时fn又变为一个函数了
fn = 3; //此时fn变为3,也就是变量的赋值覆盖了方法的声明
console.log(fn); //输出结果3
可是结果,,什么鬼。。。。
原因是控制台并没有一个统一的标准,控制台也有bug,所以我们用html运行才是最可靠的:
6、如下代码的输出?为什么
function fn(fn2){
console.log(fn2);
var fn2 = 3;
console.log(fn2);
console.log(fn);
function fn2(){
console.log('fnnn2');
}
}
fn(10);
function fn(fn2){
var fn2; //找到作用域下的变量声明,放在最前面,此时fn2为普通变量
function fn2(){
console.log('fnnn2');
} //函数声明前置,放在变量声明的后面,此时fn2变为一个函数
console.log(fn2); //所以这里输出函数
fn2 = 3; //fn2被赋值为3,覆盖前面方法的声明
console.log(fn2); //输出3
console.log(fn); //打印出函数fn
}
fn(10); //调用函数fn()
7、如下代码的输出?为什么
var fn = 1;
function fn(fn){
console.log(fn);
}
console.log(fn(fn));
相当于如下代码
var fn; //变量声明在前面,位置不变,此时fn为普通的未初始化的变量
function fn(fn){
console.log(fn);
} //现在fn变成了一个函数
fn = 1; //变量的赋值会覆盖方法的声明,所以变量赋值移动到这里,fn变成数字1
console.log( fn(fn) ); //这里是调用函数fn,但是此时fn是数字1,不能当作函数来调用,所以会报错
8、如下代码的输出?为什么
//作用域
console.log(j);
console.log(i);
for(var i=0; i<10; i++){
var j = 100;
} //这里面的变量都是全局变量,所以我们要将变量声明提前
console.log(i);
console.log(j);
因为for是控制语句,不是函数,所以它里面的变量i和j都是全局的,所以它相当于如下的代码:
var i; //变量提升,将变量声明前置到代码头部
var j;
console.log(j); //所以这里输出的是undefined,也就是表示j有声明但是未被初始化
console.log(i); //同上
for(i=0;i<10;i++){
j = 100;
}
console.log(i); //上面的for循环执行完后i变为10,所以打印出10
console.log(j); //上面的for循环结束后输出j为100,所以打印出100
9、如下代码的输出?为什么
fn();
var i = 10;
var fn = 20;
console.log(i);
function fn(){
console.log(i);
var i = 99;
fn2();
console.log(i);
function fn2(){
i = 100;
}
}
我们来排序,替换成下面的等价代码:
var i; // 变量的声明放代码顶部,i为一未初始化的变量
var fn; // 变量的声明放代码顶部,fn为一未初始化的变量
function fn(){
var i; //同样变量的声明放代码顶部,i为一未初始化的变量
function fn2(){
i = 100; //这个i由于未声明,所以在上一级作用域下找,会改变上一级作用域下i的值
} //函数声明整体提前,放在变量声明的后面,此时fn2变为一个函数了
console.log(i); //undefined,因为fn2还未被执行,所以i还是未赋值状态
i = 99; //i被赋值为99了
fn2(); //现在执行函数fn2,将fn作用域下的i 赋值为100了
console.log(i); // 所以这里打印出100
} //把函数声明整体提前,放在变量声明的后面,此时fn变为一个函数了
fn(); //它的位置不变,依旧放在变量赋值的前面,调用函数fn
i = 10; //变量赋值的位置,这里给i赋值10
fn = 20; //变量赋值的位置,到这里fn就不是函数了,而是一个数字
console.log(i); //打印全局作用域下的i,为10
10、如下代码的输出?为什么
var say = 0;
(function say(n){
console.log(n);
if(n<3) return;
say(n-1);
}( 10 ));
console.log(say);
function外部加了一个括号,所以为一个立即执行函数,里面又嵌套了say(n-1),所以为递归函数。把函数最后面的参数10传递进去,所以一开始打印的就是10,依次递减直到等n小于3,也就是当n等于2的时候退出整个循环。然后再执行最后一句,打印出say的值,注意这里并不是函数调用哦,后面没有括号。
(完)