函数的扩展

参数默认值

ES5中设置默认值非常不方便, 我们这样写:

function fun(a){
  a = a || 2;
  console.log(a);
}
fun();   //2
fun(0);  //2
fun(1);  //1

以上写法, 如果传入了参数, 但这个参数对应值的布尔型是 false, 就不起作用了。当然你也可以判断 arguments.length 是否为0来避免这个问题, 但每个函数这样写就太啰嗦了, 尤其参数比较多的时候。在 ES6 中我们可以直接写在参数表中, 如果实际调用传递了参数, 就用这个传过来的参数, 否则用默认参数。像这样:

function fun(a=2){
  console.log(a);
}
fun();   //2
fun(0);  //0
fun(1);  //1

其实函数默认参数这一点最强大的地方在于可以和解构赋值结合使用:

//参数传递
function f([x, y, z=4]){
  return [x+1, y+2, z+3];
}
var [a, b, c] = f([1, 2]);  //a=2, b=4, c=7
[[1, 2], [3, 4]].map(([a, b]) => a + b);   //返回 [3, 7]

通过上面这个例子不难发现, 不仅可以用解构的方法设置初始值, 还可以进行参数传递。当然, 这里也可以是对象形式的解构赋值。如果传入的参数无法解构, 就会报错:

function fun1({a=1, b=5, c='A'}){
  console.log(c + (a + b));
}
fun1({});   //'A6'
fun1();     //TypeError, 因为无法解构
//但这样设计函数对使用函数的码农很不友好
//所以, 技巧:
function fun2({a=1, b=5, c='A'}={}){
  console.log(c + (a + b));
}
fun2();     //'A6'

注意, 其实还有一种方法, 但不如这个好, 我们比较如下:

//fun1 比 fun2 好, 不会产生以外的 undefined
function fun1({a=1, b=5, c='A'}={}){
  console.log(c + (a + b));
}
function fun2({a, b, c}={a: 1, b: 5, c: 'A'}){
  console.log(c + (a + b));
}
//传了参数, 但没传全部参数就会出问题
fun1({a: 8});     //'A13'
fun2({a: 8});     //NaN

不过这里强烈建议, 将具有默认值的参数排在参数列表的后面。否则调用时依然需要传参:

function f1(a=1, b){
  console.log(a + b);
}
function f2(a, b=1){
  console.log(a + b);
}
f2(2);   //3
f1(, 2);  //报错
f1(undefined, 2);  //3, 注意这里不能用 null 触发默认值
  • 函数的 length 属性
    这个属性ES6 之前就是存在的, 记得length表示预计传入的形参个数, 也就是没有默认值的形参个数:
(function(a){}).length;   //1
(function(a = 5){}).length;   //0
(function(a, b, c=5){}).length;   //2
(function(...args){}).length;   //0, rest参数也不计入 length

rest 参数

rest 参数形式为 ...变量名, 它会将对应的全部实际传递的变量放入数组中, 可以用它来替代 arguments:

function f(...val){
  console.log(val.join());
}
f(1, 2);      //[1, 2]
f(1, 2, 3, 4);  //[1, 2, 3, 4]

function g(a, ...val){
  console.log(val.join());
}
g(1, 2);      //[2]
g(1, 2, 3, 4);  //[2, 3, 4]

否则这个函数 g 你的这样定义函数, 比较麻烦:

function g(a){
  console.log([].slice.call(arguments, 1).join());
}

这里需要注意2点:

  • rest参数必须是函数的最后一个参数, 它的后面不能再定义参数, 否则会报错。
  • rest参数不计入函数的 length 属性中

建议:

  • 所有配置项都应该集中在一个对象,放在最后一个参数,布尔值不可以直接作为参数。这样方便调用者以任何顺序传递参数。
  • 不要在函数体内使用arguments变量,使用rest运算符(...)代替。因为rest运算符显式表明你想要获取参数,而且arguments是一个类似数组的对象,而rest运算符可以提供一个真正的数组。
  • 使用默认值语法设置函数参数的默认值。

扩展运算符

扩展运算符类似 rest运算符的逆运算, 用 ... 表示, 放在一个(类)数组前, 将该数组展开成独立的元素序列:

console.log(1, ...[2, 3, 4], 5);  //输出1, 2, 3, 4, 5

扩展运算符的用处很多:

  • 可以用于快速改变类数组对象为数组对象, 也是用于其他可遍历对象:
[...document.querySelectorAll('li')];   //[<li>, <li>, <li>];
  • 结合 rest 参数使函数事半功倍:
function push(arr, ...val){
  return arr.push(...val);      //调用函数时, 将数组变为序列
}
  • 替代 apply 写法
var arr = [1, 2, 3];
var max = Math.max(...arr);   //3

var arr2 = [4, 5, 6];
arr.push(...arr2);     //[1, 2, 3, 4, 5, 6]

new Date(...[2013, 1, 1]);   //ri Feb 01 2013 00: 00: 00 GMT+0800 (CST)
  • 连接, 合并数组
var more = [4, 5];
var arr = [1, 2, 3, ...more];    //[1, 2, 3, 4, 5]

var a1 = [1, 2];
var a2 = [3, 4];
var a3 = [5, 6];
var a = [...a1, ...a2, ...a3];     //[1, 2, 3, 4, 5, 6]
  • 解构赋值
var a = [1, 2, 3, 4, 5];
var [a1, ...more] = a;      //a1 = 1, more = [2, 3, 4, 5]
//注意, 扩展运算符必须放在解构赋值的结尾, 否则报错
  • 字符串拆分
var str = "hello";
var alpha = [...str];    //alpha = ['h', 'e', 'l', 'l', 'o']

[...'x\uD83D\uDE80y'].length;   //3, 正确处理32位 unicode 字符

建议:使用扩展运算符(...)拷贝数组。

name 属性

name 属性返回函数的名字, 对于匿名函数返回空字符串。不过对于表达式法定义的函数, ES5 和 ES6有差别:

var fun = function(){}
fun.name;     //ES5: "", ES6: "fun"

(function(){}).name;   //""

对于有2个名字的函数, 返回后者, ES5 和 ES6没有差别:

var fun  = function baz(){}
fun.name;        //baz

对于 Function 构造函数得到的函数, 返回 anonymous:

new Function("fun").name;    //"anonymous"
new Function().name;    //"anonymous"
(new Function).name;    //"anonymous"

对于 bind 返回的函数, 加上 bound 前缀

function f(){}
f.bind({}).name;   //"bound f"

(function(){}).bind({}).name;    //"bound "

(new Function).bind({}).name;    //"bound anonymous"

箭头函数

箭头函数的形式如下:

var fun = (参数列表) => {函数体};

如果只有一个参数(且不指定默认值), 参数列表的圆括号可以省略; (如果没有参数, 圆括号不能省略)
如果只有一个 return 语句, 那么函数体的花括号也可以省略, 同时省略 return 关键字。

var fun = value => value + 1;
//等同于
var fun = function(value){
  return value + 1;
}
var fun = () => 5;
//等同于
var fun = function(){
  return 5;
}

如果箭头函数的参数或返回值有对象, 应该用 () 括起来:

var fun = n => ({name: n});
var fun = ({num1=1, num2=3}={}) => num1 + num2;

看完之前的部分, 箭头函数应该不陌生了:

var warp = (...val) => val;
var arr1 = warp(2, 1, 3);              //[2, 1, 3]
var arr2 = arr1.map(x => x * x);     //[4, 1, 9]
arr2.sort((a, b) => a - b);          //[1, 4, 9]

使用箭头函数应注意以下几点:

  • 不可以将函数当做构造函数调用, 即不能使用 new 命令;
  • 不可以在箭头函数中使用 yield 返回值, 所以不能用过 Generator 函数;
  • 函数体内不存在 arguments 参数;
  • 函数体内部不构成独立的作用域, 内部的 this 和定义时候的上下文一致; 但可以通过 call, apply, bind 改变函数中的 this。关于作用域, 集中在ES6函数扩展的最后讨论。

举几个箭头函数的实例:
实例1: 实现功能如: insert(2).into([1, 3]).after(1)insert(2).into([1, 3]).before(3)这样的函数:

var insert = value => ({
  into: arr => ({
    before: val => {
      arr.splice(arr.indexOf(val), 0, value);
      return arr;
    },
    after: val => {
      arr.splice(arr.indexOf(val) + 1, 0, value);
      return arr;
    }
  })
});
console.log(insert(2).into([1, 3]).after(1));
console.log(insert(2).into([1, 3]).before(3));

实例2: 构建一个管道(前一个函数的输出是后一个函数的输入):

var pipe = (...funcs) => (init_val) => funcs.reduce((a, b) => b(a), init_val);

//实现 2 的 (3+2) 次方
var plus = a => a + 2;
pipe(plus, Math.pow.bind(null, 2))(3);         //32

实例3: 实现 𝜆 演算

//fix = 𝜆f.(𝜆x.f(𝜆v.x(x)(v)))(𝜆x.f(𝜆v.x(x)(v)))
var fix = f => (x => f(v => x(x)(v)))(x => f(v => x(x)(v)));

建议:箭头函数取代 Function.prototype.bind,不应再用 self / _this / that 绑定 this。其次,简单的、不会复用的函数,建议采用箭头函数。如果函数体较为复杂,行数较多,还是应该采用传统的函数写法。

这里需要强调,以下情况不能使用箭头函数:

  1. 定义字面量方法
let calculator = {
  array: [1, 2, 3],
  sum: () => {
    return this.array.reduce((result, item) => result + item);     //这里的 this 成了 window
  }
};
calculator.sum();    //"TypeError: Cannot read property 'reduce' of undefined"
  1. 定义原型方法
function Cat(name) {
    this.name = name;
}
Cat.prototype.sayCatName = () => {
    return this.name;           //和上一个问题一样:这里的 this 成了 window
};
let cat = new Cat('Mew');
cat.sayCatName();               //undefined
  1. 绑定事件
const button = document.getElementById('myButton');
button.addEventListener('click', () => {
    this.innerHTML = 'Clicked button';        //这里的 this 本应该是 button, 但不幸的成了 window
});
  1. 定义构造函数
let Message = (text) => {
    this.text = text;
};
let helloMessage = new Message('Hello World!');         //TypeError: Message is not a constructor
  1. 不要为了追求代码的简短丧失可读性
let multiply = (a, b) => b === undefined ? b => a * b : a * b;    //这个太难读了,太费时间
let double = multiply(2);
double(3);      //6
multiply(2, 3); //6

函数绑定

ES7 中提出了函数绑定运算, 免去我们使用 call, bind, apply 的各种不方便, 形式如下:

objName::funcName

以下几组语句两两等同

var newFunc = obj::func;
//相当于
var newFunc = func.bind(obj);

var result = obj::func(...arguments);
//相当于
var result = func.apply(obj, arguments);

如果 :: 左边的对象原本就是右边方法中的 this, 左边可以省略

var fun = obj::obj.func;
//相当于
var fun = ::obj.func;
//相当于
var fun = obj.func.bind(obj);

:: 运算返回的还是对象, 可以进行链式调用:

$('.my-class')::find('p')::text("new text");
//相当于
$('.my-class').find('p').text("new text");

尾调用优化

尾调用是函数式编程的概念, 指在函数最后调用另一个函数。

//是尾调用
function a(){
  return g();
}
function b(p){
  if(p>0){
    return m();
  }
  return n();
}
function c(){
  return c();
}

//以下不是尾调用
function d(){
  var b1 = g();
  return b1;
}
function e(){
  g();
}
function f(){
  return g() + 1;
}

尾调用的一个显著特点就是, 我们可以将函数尾部调用的函数放在该函数外面(后面), 而不改变程序实现结果。这样可以减少函数调用栈的开销。
这样的优化在 ES6 的严格模式中被强制实现了, 我们需要做的仅仅是在使用时候利用好这个优化特性, 比如下面这个阶乘函数:

function factorial(n){
  if(n <= 1) return 1;
  return n * factorial(n - 1);
}
factorial(5);     //120

这个函数计算 n 的阶乘, 就要在内存保留 n 个函数调用记录, 空间复杂度 O(n), 如果 n 很大可能会溢出。所以进行优化如下:

"use strict";
function factorial(n, result = 1){
  if(n <= 1) return result;
  return factorial(n - 1, n * result);
}
factorial(5);     //120

当然也可以使用柯里化:

var factorial = (function factor(result, n){
  if(n <= 1) return result;
  return factor(n * result, n - 1);
}).bind(null, 1);
factorial(5);     //120

函数的尾逗号

这个仅仅是一个提案: 为了更好地进行版本控制, 在函数参数尾部加一个逗号, 表示该函数日后会被修改, 便于版本控制器跟踪。目前并未实现。

作用域

这里仅仅讨论 ES6 中的变量作用域。除了 let 和 const 定义的的变量具有块级作用域以外, varfunction 依旧遵守词法作用域, 词法作用域可以参考博主的另一篇文章javascript函数、作用域链与闭包

首先看一个例子:

var x = 1;
function f(x, y=x){
  console.log(y);
}
f(2);    //2

这个例子输出了2, 因为 y 在初始化的时候, 函数内部的 x 已经定义并完成赋值了, 所以, y = x 中的 x 已经是函数的局部变量 x 了, 而不是全局的 x。当然, 如果局部 x 变量在 y 声明之后声明就没问题了。

var x = 1;
function f(y=x){
  let x = 2
  console.log(y);
}
f();    //1

那如果函数的默认参数是函数呢?烧脑的要来了:

var foo = "outer";
function f(x){
  return foo;
}
function fun(foo, func = f){
  console.log(func());
}
fun("inner");   //"outer"

如果基础好, 那就根本谈不上不烧脑。因为, 函数中的作用域取决于函数定义的地方, 函数中的 this 取决于函数调用的方式。(敲黑板)
但如果这样写, 就是 inner 了, 因为func默认函数定义的时候 fun内的 foo 已经存在了。

var foo = "outer";
function fun(foo, func = function(x){
  return foo;
}){
  console.log(func());
}
fun("inner");   //"inner"

技巧: 利用默认值保证必需的参数被传入, 而减少对参数存在性的验证:

function throwErr(){
  throw new Error("Missing Parameter");
}
function fun(necessary = throwErr()){
  //...如果参数necessary没有收到就使用参数, 从而执行函数抛出错误
}

//当然也可以这样表示一个参数是可选的
function fun(optional = undefined){
  //...
}

箭头函数的作用域和定义时的上下文一致, 但可以通过调用方式改变:

window && (window.name = "global") || (global.name = "global");
var o = {
  name: 'obj-o',
  foo: function (){
    setTimeout(() => {console.log(this.name); }, 500);
  }
}

var p = {
  name: 'obj-p',
  foo: function (){
    setTimeout(function(){console.log(this.name); }, 1000);
  }
}

o.foo();    //"obj-o"
p.foo();    //"global"

var temp = {
  name: 'obj-temp'
}

o.foo.bind(temp)();     //"obj-temp"
o.foo.call(temp);     //"obj-temp"
o.foo.apply(temp);     //"obj-temp"

p.foo.bind(temp)();     //"global"
p.foo.call(temp);     //"global"
p.foo.apply(temp);     //"global"
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容

  • 函数参数的默认值 基本用法 在ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法。 上面代码检查函数l...
    呼呼哥阅读 3,345评论 0 1
  • 1、函数参数的默认值1)基本用法在ES6之前,不能直接为函数的参数指定默认值,为了避免这个问题,通常需要先判断一下...
    秋天de童话阅读 366评论 0 0
  • 函数参数的默认值 ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。 参数变量是默认声明的,所以不能用...
    yujiawei007阅读 80评论 0 0
  • 今天的一件事,不禁让我想起前些日子的后悔话题。是的,我后悔了,为自己的冲动后悔,为自己的无理取闹惭愧,原来有些时候...
    小帅在成长阅读 178评论 0 0
  • 重庆的天气又下雨了,你的天空会是怎样呢? 重庆的天气总是会在刹那间,让你早上一醒来经历四季的感觉,就像顽皮的小孩,...
    奔跑的胖胖蜗牛阅读 232评论 0 0