ES6深入学习(二)关于函数
栏目: JavaScript · 发布时间: 5年前
内容简介:学习是一个不断积累的过程,以前我只是输入并不懂输出。后来发现输入其实是一个被动的操作,知识点看过后以为自己懂了,可真正准备输出时却发现掌握的一点也不牢固,只有输入输出同步进行才能不断检测自己掌握的程度,还能与大家交流学习。若有错误或者建议欢迎指出,我会虚心改正。(白羊的我希望能坚持久一点,不半途而废~)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》—— 函数
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
数据挖掘导论
(美)Pang-Ning Tan、Michael Steinbach、Vipin Kumar / 机械工业出版社 / 2010-9 / 59.00元
本书全面介绍了数据挖掘的理论和方法,着重介绍如何用数据挖掘知识解决各种实际问题,涉及学科领域众多,适用面广。 书中涵盖5个主题:数据、分类、关联分析、聚类和异常检测。除异常检测外,每个主题都包含两章:前面一章讲述基本概念、代表性算法和评估技术,后面一章较深入地讨论高级概念和算法。目的是使读者在透彻地理解数据挖掘基础的同时,还能了解更多重要的高级主题。 本书特色 ·包含大量的图表、......一起来看看 《数据挖掘导论》 这本书的介绍吧!