ES6深入学习(二)关于函数
栏目: JavaScript · 发布时间: 6年前
内容简介:学习是一个不断积累的过程,以前我只是输入并不懂输出。后来发现输入其实是一个被动的操作,知识点看过后以为自己懂了,可真正准备输出时却发现掌握的一点也不牢固,只有输入输出同步进行才能不断检测自己掌握的程度,还能与大家交流学习。若有错误或者建议欢迎指出,我会虚心改正。(白羊的我希望能坚持久一点,不半途而废~)ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法。对于函数的命名参数如果不显示传值则默认为undefined这种写法的确定在于,如果参数x或者y赋值了,但是对应的布尔值为false,则该赋值不起
学习是一个不断积累的过程,以前我只是输入并不懂输出。后来发现输入其实是一个被动的操作,知识点看过后以为自己懂了,可真正准备输出时却发现掌握的一点也不牢固,只有输入输出同步进行才能不断检测自己掌握的程度,还能与大家交流学习。若有错误或者建议欢迎指出,我会虚心改正。(白羊的我希望能坚持久一点,不半途而废~)
一、函数参数的默认值
ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法。对于函数的命名参数如果不显示传值则默认为undefined
function foo(x,y) {
x = x || 'Hello';
y = y || 'World';
console.log(x,y)
}
foo(0,'Thea');//Hello Thea
复制代码
这种写法的确定在于,如果参数x或者y赋值了,但是对应的布尔值为false,则该赋值不起作用。这种情况下更安全的选择是通过typeof检查参数类型:
function foo(x,y) {
debugger
x = (typeof x !== 'undefined') ? x : 'Hello';
y = (typeof y !== 'undefined') ? y : 'World';
console.log(x,y);
}
foo(false,'Thea');//false "Thea"
复制代码
-
1.1 ES6中的默认参数值
ES6简化了为形参提供默认值的过程,如果没为参数传入值则提供一个初始值:
function foo(x,y='World'){
console.log(x,y)
}
foo('I love')//I love World
复制代码
除了简洁,ES6 的写法还有两个好处:首先,阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;其次,有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行。
-
1.2值得注意:
1、参数变量是默认声明的,函数体中不能用let或const再次声明,否则报错
function foo(x,y) {
let x = 'Hello';//Uncaught SyntaxError
}
复制代码
2、使用参数默认值时,函数不能有同名参数。
function foo(x,x,y=3) {
//...
}
//Uncaught SyntaxError: Duplicate parameter name not allowed in this context
复制代码
3、参数默认值不是传值的,而是每次都重新计算默认值的表达式的值(参数默认值是惰性求值的)初次解析函数声明时不会计算默认值的表达式值,只有当调用foo()函数且不传参数时才会调用。
let x = 3;
function foo(,p = x + 1){
console.log(p)
}
foo()//4
x = 5;
foo()//6
复制代码
参数p的默认值是x+1,每次调用函数foo都会重新计算x+1,而不是默认等于4。 正因为默认参数是在函数调用时求值,所以可以使用先定义的参数作为后定义参数的默认值
function foo(x,y=x) {
return x+y
}
foo(1) //2
复制代码
4、参数默认值的位置
通常情况下,定义了默认值的参数应该是函数的尾参数。因为这样容易看出到底省略了哪些参数,实际上如果非尾部的参数设置默认值,这个参数是没法省略的,(除非不为其后参数传入值或主动为第二个参数传入undefined才会使用这个默认参数)null没有这个效果,因为null是一个合法值。
-
1.3函数的lengh属性
默认参数值对arguments对象的影响。 在ES5非严格模式下,函数命名参数的变化会体现在argumnets对象中;
function foo(x,y) {
console.log(x === arguments[0]); // true
y = 3;
console.log(y === arguments[1]); // true
}
foo.length //2
复制代码
在非严格模式下,命名参数的变化会同步更新到arguments对象中,所以x,y被赋予新值时,最终===全等比较的结果为true。然而在ECMAScript5的严格模式下,取消了arguments随之改变的行为。无论参数如何改变,arguments对象不再随之改变。
在ES6中,如果一个函数使用了默认参数值,无论是否显示定义了严格模式,arguments对象的行为都将与ES5严格模式下保持一致。
function foo(x,y=1){
console.log(arguments.length);
console.log(x === arguments[0]);
console.log(y === arguments[1]);
}
foo(1);//1,true,false
foo(1,2);//2,true,true
foo.length;//1
复制代码
指定了默认值后,函数的length属性将返回没哟指定默认值的参数个数,因为length属性的含义是,该函数预期传入的参数的个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理rest参数也不会计入length属性。
-
1.4作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context),等到初始化结束,这个作用域会消失,不设置默认值是不会出现。
const x = 1;
function foo(x,y = x) {
console.log(y);
}
foo(3)//3
复制代码
参数y的默认值等于变量x,调用函数foo时,参数形成一个单独的作用域,在这个作用域里,默认值变量x指向第一个参数x,而不是全局变量x。
const x = 3;
function foo(y = x) {
let x = 2;
console.log(y)
}
foo();//3
复制代码
调用函数foo时,参数y=x形成一个单独的作用域,这个作用域里x没定义,所以指向外层的全局变量x。函数体内部的局部变量x不会形象默认值变量x。如果此时全局变量x不存在,就会报错。
const x = 3;
function foo(x = x) {
//...
}
foo()//ReferenceError: x is not defined
复制代码
上述代码中x=x形成一个单独的作用域,实际执行的是 let x = x,由于临时死区(与let的行为类似)原因。
若参数的默认值是一个函数,该函数的作用域也遵循这个规则:
let z = 'z-outer';
function foo(func = () => z) {
let z = 'z-inner';
console.log(func());
}
foo()//z-outer z指向外层全局变量z
复制代码
请看下面一个复杂的例子:
var d = 'd-outer';
function foo(d,y=() => {d = 'd-argumnets';}){
var d = 'd-inner';
y();
console.log(d);
}
foo()//d-inner
d; // d-outer
复制代码
上述代码中,函数foo的参数形成一个单独作用域,这个作用域里先声明了变量d,然后声明了一个默认值是匿名函数的y。匿名函数内部的变量d指向该参数作用域里的第一个参数d。函数foo内部又声明了一个内部的变量d,这个d与参数作用域里的d不是同一个变量,所以执行匿名函数y后,函数foo内部的d以及全局变量d的值都没有改变。
如果将函数foo内部的var d = 'd-inner';的var 去掉,那么函数foo内部的d就指向第一个参数d,与匿名函数内部的d同一个参数,外层全局变量d仍不受影响。
var d = 'd-outer';
function foo(d,y=() => {d = 'd-argumnets';}){
d = 'd-inner';
y();
console.log(d);
}
foo()//d-argumnets
d; // d-outer
复制代码
函数参数有自己的作用域和临时死区,其与函数体的作用域是各自独立的,也就是说参数的默认值不可访问函数体内声明的变量。
二、处理无命名参数
JavaScript的函数语法规定,无论函数已定义的命名参数有多少,都不限制调用时传入的实际参数数量,调用时总可以传入任意数量的参数。
ES6引入不定参数,在函数的命名参数前添加三个点(...)就表名这是一个不定参数,rest参数,用于获取函数的多余参数,该参数为一个数组,包含着自它之后传入的所有参数,通过这个数组名即可逐一访问里面的参数。 rest参数之后不能再有其他参数,只能作为最后一个参数,且每个函数最多只能声明一个不定参数,否则会报错 函数的length也不包括rest参数。
function foo(...arrs) {
for(let val of arrs){
console.log(val)
}
}
foo(1,2,3);//1,2,3
复制代码
知识点扩展:
for...of 语句创建一个循环来迭代可迭代的对象。在 ES6 中引入的 for...of 循环,以替代 for...in 和 forEach() ,并支持新的迭代协议。for...of 允许你遍历 Arrays(数组), Strings(字符串), Maps(映射), Sets(集合)等可迭代的数据结构等。
用法:
for (variable of iterable) {
statements
}
复制代码
variable:每个迭代的属性值被分配给该变量。 iterable:一个具有可枚举属性并且可以迭代的对象。
不定参数的设计初衷是代替JavaScript的arguments对象,起初在ECMAScript4草案中,arguments对象被移除并添加了不定参数的特性,从而可以传入不限制数量的参数,但ECMAScript4从未被标准化,这个想法被搁置下来,直到重新引入了ES6标准,唯一的区别是arguments对象依然存在。如果声明函数时定义了不定参数,则在函数被调用时,arguments对象包含了所有传入函数的参数。
function foo(a,...b){
console.log(b.length);
console.log(arguments.length);
}
foo.length;//1
foo(1,2,3,4);//3,4
复制代码
-
严格模式
从ES5开始,函数内部可以设定为严格模式。ES6做了一点修改,规定只要函数参数使用了默认值、解构赋值或者扩展运算符,那么函数就不能显式设定为严格模式,否则会报错。这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。
有两种方法可以规避这种限制
//1、设定全局性的严格模式
'use strict';
function foo(x,y=x) {
//statements
}
//2、把函数包在一个无参数的立即执行函数的里面
const doSomething = (function() {
'use strict';
return function(...a) {
for(let val of a){
console.log(val)
}
}
}())
复制代码
-
name属性
函数的name属性,返回该函数的函数名。注意:函数name属性的值不一定引用同名变量,它只是协助调试用的额外信息,所以不能使用name属性的值来获取对于函数的引用。
function foo(){}
foo.name //'foo'
复制代码
如果将一个匿名函数赋值给一个变量,ES5 的name属性,会返回空字符串,而 ES6 的name属性会返回实际的函数名。
var foo = function() {}
//ES5
foo.name //'';
//ES6
foo.name //"foo"
复制代码
如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的name属性都返回这个具名函数原本的名字。函数表达式有一个名字,这个名字比函数本身被赋值的变量权重高。
let foo = function bar() {};
//ES5、ES6
foo.name //bar
复制代码
另外还有两个特例:通过bind()函数创建的函数,其名称带有“bound”前缀;通过Function构造函数创建的函数,其名称将是“anonymous”。
var foo = function() {};
console.log(foo.bind().name);//bound foo
console.log(new Function().name);//anonymous
复制代码
-
箭头函数
ES6运行使用箭头(=>)定义函数,箭头函数同样也有一个name属性,这与其他函数的规则相同。
var foo = x => x;
//等价于
var foo = function(x){
return x;
}
复制代码
如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分,如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并显示地定义一个返回值。由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
与传统JavaScript函数不同点:
没有this、supper、arguments和new.target绑定:箭头函数的这些值由外围最近一层非箭头函数决定。函数内部的this值不可被改变,在函数的生命周期内始终保持一致。
不能通过new关键字调:箭头函数没有[[Construct]]方法,所以不能被用作构造函数,如果通过new关键字调用箭头函数,程序会抛出错误。由于不能通过new关键字调用箭头函数,因而没有构建原型的需求,所以箭头函数不存在prototype属性
不支持arguments对象:该对象在函数体内不存在。如果要用,可以用命名参数或 rest 参数代替。
不支持重复命名参数:无论在严格还是非严格模式下,箭头函数都不支持重复的命名参数,而在传统函数规定中,只有在严格模式下才不能有重复的命名参数
function foo(){
setTimeout(() => {
console.log('id:',this.id);
},1000)
}
var id = 1;
foo.call({id:2});//id:2
复制代码
setTimeout的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到 1000 毫秒后。如果是普通函数,执行时this应该指向全局对象window,这时应该输出1。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 2})而不是指向运行时所在的作用域,所以输出的是2。
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭头函数
setInterval(() => this.s1++, 1000);
// 普通函数
setInterval(function () {
this.s2++;
}, 1000);
}
var timer = new Timer();
setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0
复制代码
Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this绑定定义时所在的作用域(即Timer函数),后者的this指向运行时所在的作用域(即全局对象)。所以,3100 毫秒之后,timer.s1被更新了 3 次,而timer.s2一次都没更新。
-
尾调用优化
尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。
function foo(){
return bar();//尾调用
}
复制代码
在ES5的引擎中,尾调用的实现与其他函数调用的实现类似:创建一个新的栈帧(stack frame),将其推入调用栈来表示函数调用,也就是说循环调用中,每一个未用完的栈帧都会被保存在内存中,当调用栈变得过大时,会造成程序问题。
以下三种情况都不属于尾调用
//在调用函数g后还有赋值操作,即使语义完全一样
function f(x) {
let y = g(x);
return y;
}
//调用后还有操作,即使写在一行内
function f(x) {
return g(x) + 1;
}
//调用后实际还有一个renturn undefined操作
function f(x) {
g(x);
}
复制代码
尾调用不一定出现在函数尾部,只要是最后一步操作即可。
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);
}
复制代码
尾调用之所以与其他调用不同,就在于它的特殊的调用位置。
我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。
function foo(){
let x = 1,
y = 2;
return bar(x+y);
}
foo();
//等同于
function foo() {
return bar(3);
}
f();
//等同于
bar(3)
复制代码
如果函数g不是尾调用,函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除f(x)的调用帧,只保留g(3)的调用帧。
这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。
function addOne(a){
var one = 1;
function inner(b){
return b + one;
}
return inner(a);
}
复制代码
上面的函数不会进行尾调用优化,因为内层函数inner用到了外层函数addOne的内部变量one。
相关文章:ES6深入学习(一)块级作用域详解( juejin.im/post/5cb6c8… )
如有错误或者建议欢迎指出,我一定快马加鞭的改正~一起学习交流
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 深入理解 JavaScript 函数
- 【4】JavaScript 基础深入——函数、回调函数、IIFE、理解this
- 深入理解 Java 函数式编程,第 5 部分: 深入解析 Monad
- 深入学习javascript函数式编程
- [译] 深入理解 JavaScript 回调函数
- 重读《深入理解ES6》—— 函数
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
PHP and MySQL Web Development
Luke Welling、Laura Thomson / Sams / July 25, 2007 / $49.99
Book Description PHP and MySQL Web Development teaches you to develop dynamic, secure, commerical Web sites. Using the same accessible, popular teaching style of the three previous editions, this b......一起来看看 《PHP and MySQL Web Development》 这本书的介绍吧!