Skip to content

Latest commit

 

History

History
543 lines (473 loc) · 15.1 KB

函数的扩展.md

File metadata and controls

543 lines (473 loc) · 15.1 KB

参数默认值

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 触发默认值

这里我们还需要单独讨论一下默认参数对 arguments 的影响:

function foo(a = 1){
  console.log(a, arguments[0]);
}

foo();            //1 undefined
foo(undefined);   //1 undefined
foo(2);           //2 2
foo(null);        //null null

很明显,默认参数并不能加到 arguments 中。

  • 函数的 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"