2021.11.2:JS(六):函数 作用域 var与let 解构赋值
定义
function 函数名(参数){ ... } //或 var 函数名 = function(参数){ ... }
参数
调用时,JS允许多传入参数,但是多传入的参数不会起作用;也允许少传入参数,但如果其中用到了相关参数可能导致返回一个undefined。
关键字arguments
JS还有一个免费赠送的关键字arguments,它只在函数内部起作用,并且永远指向函数传入的所有参数,它类似一个Array但并不是一个Array:
function foo(x) { console.log('x= '+x);//10 for(var i=0;i<arguments.length;i++){ console.log('arg '+i+' = '+arguments[i]);//10,20,30 } } foo(10,20,30);
利用arguments,我们可以获取调用者传入的所有参数。也就是说,即使函数不定义任何参数,还是可以拿到参数的值:
function abs() { if (arguments.length === 0) { return 0; } var x = arguments[0]; return x >= 0 ? x : -x; } abs(); // 0 abs(10); // 10 abs(-9); // 9
实际上arguments最常用于判断传入参数的个数:
// foo(a[, b], c) // 接收2~3个参数,b是可选参数,如果只传2个参数,b默认为null: function foo(a, b, c) { if (arguments.length === 2) { // 实际拿到的参数是a和b,c为undefined c = b; // 把b赋给c b = null; // b变为默认值 } // ... }
要把中间参数b变为“可选”参数,就只能通过arguments判断,然后重新调整参数并赋值。
rest参数
由于JS允许接受任意个参数,于是我们就不得不用arguments来获取所有参数:
function foo(a, b) { var i, rest = []; if (arguments.length > 2) { for (i = 2; i<arguments.length; i++) { rest.push(arguments[i]); } } console.log('a = ' + a); console.log('b = ' + b); console.log(rest); }
为了获取除了已定义参数a、b外的参数,除了用arguments并从索引2开始循环外,还可以用rest参数:
function foo(a, b, ...rest) { console.log('a = ' + a); console.log('b = ' + b); console.log(rest); } foo(1, 2, 3, 4, 5); // 结果: // a = 1 // b = 2 // Array [ 3, 4, 5 ] foo(1); // 结果: // a = 1 // b = undefined // Array []
rest参数只能写在最后,前边用...标识。传入的参数会先绑定明确指定的参数,多余的参数以Array的形式交给变量rest,所以,不再需要arguments我们就获得了全部参数。
如果传入的参数连明确参数都没有填满,那么rest会接收一个空数组[ ].
变量作用域
JS的函数可以嵌套,内部函数的变量可以访问外部函数定义的变量。
JS的函数在查找变量时,都是从自身函数定义开始,从“内”向“外”查找。如果内部函数定义了与外部函数重名的变量,则内部函数的变量将“屏蔽”外部函数的变量。
变量提升
JS的函数定义有个特点——它会先扫描整个函数体的语句,再把所有变量提升到函数顶部。
就是说,不管我们在函数哪个位置定义了一个变量,这个变量总会被视为在函数顶部声明。但是直到赋值的位置处,这个变量才会被正确赋值。在此之前,是可以访问这个变量的,只是它的值会被视为Undefined。
由于JS的这一怪异的“特性”,我们在函数内部定义变量时,要严格遵守“首先申明所有变量”的规则。最常见的做法是用一个var申明函数内部用到的所有变量:
function foo(){ var x=1,//x初始化为1 y=x+1,//y初始化为2 z,i;//z和i初始化为undefined //其他语句 .... }
全局作用域
不在任何函数内定义的变量就具有全局作用域。实际上,JS默认有一个全局对象window,全局作用域的变量实际上被绑定到window的一个属性:
var course = 'Learn JavaScript'; alert(course);//'Learn JavaScript' alert(window.course); //'Learn JavaScript'
因此直接访问全局变量course和访问window.course是完全一样的。
由于函数可以通过var foo = function() {}这种形式定义,所以顶层函数也是一个全局变量,并绑定到window对象。
JS只有一个全局作用域。任何变量(包括函数)如果没有在当前函数作用域中找到,就会继续往上查找,如果最后在全局作用域中也没有找到,会报错ReferenceError。
名字空间
全局变量会绑定到window上,不同的JS文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。
减少冲突的一个方法是把自己的所有变量和函数全部绑定到一个全局变量中。例如:
//唯一的全局变量MYAPP var MYAPP = {} ; //其他变量 MYAPP.name = 'myapp'; MYAPP.version = 1.0; //其他函数 MYAPP.foo = function() { return 'foo'; };
把自己的代码全部放在唯一的名字空间MYAPP中,会大大减少全局变量冲突的可能。
许多著名的JavaScript库都是这么干的:jQuery、YUI、underscore等等。
局部作用域
在JS中,变量作用域是针对函数而言的,这就意味着,我们在代码块{ }中定义的变量仍能在代码块以外使用:
function foo() { for (var i=0; i<100; i++) { // } i += 100; // 仍然可以引用变量i }
为了解决块级作用域,ES6引入了新的关键字let,用let替代var可以申明一个块级作用域的变量:
for(let i=0;i<100;i++){ sum+=i; } //SyntaxError: i+=1;
常量
由于var和let申明的是变量,如果要申明一个常量,在ES6之前也是不行的,我们通常用全部大写的变量来表示“这是一个常量”:
var PI =3.14;
ES6标准引入了新的关键字const来定义常量,const与let都具有块级作用域:
const PI = 3.14; PI = 3;//无法修改,但是有时不报错 PI; //3.14
解构赋值
从ES6开始,JS引入了解构赋值,可以同时对一组变量进行赋值:
//传统方法 var array = ['hello', 'JavaScript', 'ES6']; var x = array[0]; var y = array[1]; var z = array[2]; //解构赋值 var [x, y, z] = ['hello', 'JavaScript', 'ES6'];
与其它编程语言相比,JS对数组元素进行解构赋值时,多个变量要用[...]括起来。
各种情形
①如果需要从一个对象中取出若干属性,也可以使用解构赋值,便于快速获取对象的指定属性:
var person = { name: '小明', age: 20, gender: 'male', passport: 'G-12345678', school: 'No.4 middle school' }; var {name, age, passport} = person;
②对一个对象进行解构赋值时,同样可以直接对嵌套的对象属性进行赋值,只要保证对应的层次是一致的即可:
var person = { name: '小明', age: 20, gender: 'male', passport: 'G-12345678', school: 'No.4 middle school', address: { city: 'Beijing', street: 'No.1 Road', zipcode: '100001' } }; var {name, address: {city, zip}} = person;
③解构时,如果对应的属性不存在,变量将被赋值为undefined。
④如果要用另一个变量名来获取某个不同名的属性,可以用下边的语法:
//把passport属性赋值给id let {name, passport:id} = person;
⑤解构时还可以使用默认值,这样就避免了不存在的属性返回undefined的问题:
var {name, single=true} = person;
⑥有时,如果变量已经被声明了,再次解构赋值的时候,正确的写法也会报语法错误:
//声明变量: var x,y; //解构赋值: {x, y} = { name: '小明', x: 100, y: 200}; // 语法错误: Uncaught SyntaxError: Unexpected token =
这是因为JS引擎把{开头的语句当做了块处理,于是=不再合法。解决方法是用小括号括起来:
({x, y} = { name: '小明', x: 100, y: 200});
以上①~⑥都是对对象属性而言,所以是用{ }进行解构赋值的。
使用场景
解构赋值在许多时候可以大大简化代码。
①交换两个变量x、y的值,不再需要临时变量:
var x=1 , y=2; [x,y] = [y,x];
②获取当前页面的域名和路径:
var {hostname:domain , pathname:path} = location;
③如果一个函数接收一个对象作为参数,那么,可以用解构直接把对象的属性绑定到变量中。例如,下面的函数可以快速创建一个Date对象:
function buildDate( {year,month,day,hour=0,minute=0,second=0}){ return new Date(year+'-'+month+'-'+day+' '+hour+':'+minute+':'+second); }
它的方便之处在于传入的对象只需要year、month、day这三个属性:
buildDate({year:2017,month:1,day:1}); // Sun Jan 01 2017 00:00:00 GMT+0800 (CST)
也可以传入hour、minute、second属性:
buildDate({ year: 2017, month: 1, day: 1, hour: 20, minute: 15 }); // Sun Jan 01 2017 20:15:00 GMT+0800 (CST)
使用解构赋值可以减少代码量,但是需要在支持ES6解构赋值特性的浏览器中才能正常运行。