函数声明

函数声明与闭包

参数

arguments

JavaScript 的函数里面的 arguments 对象有 .lengh 属性和可以通过 [] 访问,但是确实从 Object.prototype 继承的。很多时候都会用 Array.prototype.slice.call(arguments) 或者 Array.from(arguments) 转成数组。Brendan Eich 本人也承认 arguments 的设计是因为当时只花了十天所以整得太糙了。在正式规范化 JavaScript 的时候,Microsoft 曾经有人提出把 arguments 改成真正的 Array,BE 本人甚至都打算动手改实现了,但是 MS 那边回去商量了下又回来觉得多一事不如少一事,不改了。于是这个糟糕的设计就从此成为了规范 … 这是 1997 年的第一版 ES 规范。

除了 arguments.callee 之外,还有一个神奇的 quirk,那就是 arguments 和实际的参数变量之间的迷之绑定。规范里是这么说的:

In the case when iarg is less than the number of formal parameters for the function object, this property shares its value with the corresponding property of the activation object. This means that changing this property changes the corresponding property of the activation object and vice versa. The value sharing mechanism depends on the implementation.

换言之,假设一个函数的第一个参数是 a,当你修改 a 的值的时候,arguments[0] 也会同步变化:

(function (a) {
  console.log(arguments[0] === a); // -> true
  console.log(a); // -> 1

  // 修改 arguments
  arguments[0] = 10;
  console.log(a); // -> 10

  // 修改参数变量
  a = 20;
  console.log(arguments[0]); // -> 20
})(1, 2);

后面的事情你也知道了,ES 规范是要向后兼容的,而且上面的这个 quirk 使得它在引擎实现中需要很多特殊处理,一旦改动,兼容性影响巨大,所以它永远也改不了了。据说在 ES5 讨论时也有人提出要把 arguments 改成 Array 的 subclass,但是很快就不了了之,只是在 strict mode 下对 arguments.callee 和上面的绑定 quirk 进行了限制。直到 ES6 终于对 arguments 提供了一个替代品 - rest parameters:

function foo(...args) {
  // 这里 args 终于是真正的 Array 了!
}

BE 本人并没有提到为什么一开始会把 arguments 设计成对象,因此我们也只能做猜测。但一个合理的推测是,ES1 里面的 Array.prototype 其实很弱,只有四个方法:toString, join, reverse 和 sort - 连 push, pop, shift, unshift, splice 都没有!而 forEach, filter, map, reduce 这些有用的方法更是 ES5 才添加进来的。所以当时 arguments 就算真的继承自 Array 貌似也没什么大用,所以就这样被放过了 … 当然,这只是我们的猜测,估计 BE 自己今天也说不清自己当时为什么这么干的了吧。

Closure: 闭包

  • A closure is a function that has access to the parent scope, even after the scope has closed.

  • A closure is the combination of a function and the lexical environment within which that function was declared.

Closure

闭包本身是含有自由变量的代码块,在 JavaScript 中我们常用的闭包则是本身的词法作用域与变量保留相结合的表现,首先回顾下一个基本的词法作用域的用法:

function init() {
  const name = "Mozilla";
  function displayName() {
    alert(name);
  }
  displayName();
}
init();

函数 init() 创建了一个局部变量 name,然后定义了名为 displayName() 的函数。displayName() 是一个内部函数:定义于 init() 之内且仅在该函数体内可用。displayName() 没有任何自己的局部变量,然而它可以访问到外部函数的变量,即可以使用父函数中声明的 name 变量。注意,这里是直接执行外部的 init 函数,下面看一个闭包的例子

function makeFunc() {
  const name = "Mozilla";
  function displayName() {
    alert(name);
  }
  return displayName;
}

const myFunc = makeFunc();
myFunc();

运行这段代码的效果和之前的 init() 示例完全一样:字符串 “Mozilla” 将被显示在一个 JavaScript 警告框中。其中的不同 — 也是有意思的地方 — 在于 displayName() 内部函数在执行前被从其外围函数中返回了。这段代码看起来别扭却能正常运行。通常,函数中的局部变量仅在函数的执行期间可用。一旦 makeFunc() 执行过后,我们会很合理的认为 name 变量将不再可用。虽然代码运行的没问题,但实际并不是这样的。这个谜题的答案是 myFunc 变成一个闭包了。闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。在我们的例子中,myFunc 是一个闭包,由 displayName 函数和闭包创建时存在的 “Mozilla” 字符串形成。

避免闭包

在真实的开发中我们常常会使用闭包这一变量保留的特性来传递变量到异步函数中,不过闭包也往往会使程序出乎我们的控制,譬如在下面这个简单的循环中,我们本希望能够打印出 0~9 这几个数

for (const i = 0; i < 10; i++) {
  setTimeout(() => {
    console.log(i), 1000;
  });
}

不过所有输入的 i 的值都是 10,这与我们的期望产生了很大的偏差。因此我们在部分情况下需要破坏闭包而获取真实的变量值。

将异步获取值保留到新增的闭包中

我们可以考虑加一层闭包,将 i 以函数参数形式传递给内层函数:

    function init3() {
      const pAry = document.getElementsByTagName("p");
      for( const i=0; i<pAry.length; i++ ) {
       (function(arg){
           pAry[i].onclick = function() {
              alert(arg);
           };
       })(i);//调用时参数
      }
    }

或者在新增的闭包中将i以局部变量形式传递给内部函数中

function init4() {
  const pAry = document.getElementsByTagName("p");
  for (const i = 0; i < pAry.length; i++) {
    (function () {
      const temp = i; //调用时局部变量
      pAry[i].onclick = function () {
        alert(temp);
      };
    })();
  }
}

将变量值保留到作用域之外

在 DOM 环境中,我们可以将变量值存储到要操作的 DOM 对象中

function init() {
  const pAry = document.getElementsByTagName("p");
  for (const i = 0; i < pAry.length; i++) {
    pAry[i].i = i;
    pAry[i].onclick = function () {
      alert(this.i);
    };
  }
}

也可以将变量i保存在匿名函数本身

function init2() {
  const pAry = document.getElementsByTagName("p");
  for (const i = 0; i < pAry.length; i++) {
    (pAry[i].onclick = function () {
      alert(arguments.callee.i);
    }).i = i;
  }
}
const b = fun(0).fun(1).fun(2).fun(3); //undefined,0,1,2

/*一开始fun(n=undefined,o=undefined)*/
/*
    const b = fun(n=0,o=undefined); 执行后,相对应改变局部变量n和o.
    执行console.log(undefined),返回:
    const b={
        fun:执行fun(n=1,o=0);console.log(0),返回:{
            fun:执行fun(n=2,o=1);console.log(1),返回:{
                fun:执行fun(n=3,o=2);console.log(2),返回:{
                    fun:function(3){
                        return fun(m,3)由于没有执行所以n不会赋值为undefined,o也不会赋值为3
                    }
                }
            }
        }
    }
*/

JavaScript Function

在 ES5 中,可以利用function关键字声明一个函数。(function() {})() 这种定义方式,完全等价于 (function() {}).call(window [ES5-strict: undefined)这种方式。注意,在 ES5 以及之前的语法中,经常将 Function 作为类或者类的某种实现手段,不过鉴于 ES6 中已经引入了完整的 Class 的概念,所以这里将全部的 Function 以及相关概念放到了本章。

Function Params: 函数参数

参数

Arguments: 自动注入的参数对象

在 NodeJS 中,会自动在函数内注入一个 arguments 对象:

(() => arguments)(1, 2, 3) // => uncaught reference error (function() { return arguments; })(1, 2, 3) // [1, 2, 3]

而在 ES6 中,可以使用扩展运算符来方便地获取全部的参数:

((...args) => args)(1, 2, 3) // => [1,2,3]

默认参数与可选参数

当我们构造一个提供配置的对象,并且需要这个对象的属性携带默认值时,解构特性就派上用场了。举个例子,jQuery 的ajax函数使用一个配置对象作为它的第二参数,我们可以这样重写函数定义:

jQuery.ajax = function (
  url,
  {
    async = true,
    beforeSend = noop,
    cache = true,
    complete = noop,
    crossDomain = false,
    global = true,
    // ... 更多配置
  }
) {
  // ... do stuff
};

同样,解构也可以应用在函数的多重返回值中,可以类似于其他语言中的元组的特性:

function returnMultipleValues() {
  return [1, 2];
}
const [foo, bar] = returnMultipleValues();

单纯可选参数

上面所讲的,都是基于默认参数构造出的可选参数,而如果需要使用扁平化的可选参数,可以借鉴如下实现:

    function example( err, optionalA, optionalB, callback ) {
        // retrieve arguments as arrayvar args = new Array(arguments.length);
        for(const i = 0; i < args.length; ++i) {
            args[i] = arguments[i];
        };

        // first argument is the error object// shift() removes the first item from the// array and returns it
        err = args.shift();

        // if last argument is a function then its the callback function.// pop() removes the last item in the array// and returns itif (typeof args[args.length-1] === 'function') {
            callback = args.pop();
        }

        // if args still holds items, these are// your optional items which you could// retrieve one by one like this:if (args.length > 0) optionalA = args.shift(); else optionalA = null;
        if (args.length > 0) optionalB = args.shift(); else optionalB = null;

        // continue as usual: check for errorsif (err) {
            return callback && callback(err);
        }

        // for tutorial purposes, log the optional parametersconsole.log('optionalA:', optionalA);
        console.log('optionalB:', optionalB);
        console.log('callback:', callback);

        /* do your thing */

    }

    // ES6 with shorter, more terse codefunction example(...args) {
        // first argument is the error objectconst err = args.shift();
        // if last argument is a function then its the callback functionconst callback = (typeof args[args.length-1] === 'function') ? args.pop() : null;

        // if args still holds items, these are your optional items which you could retrieve one by one like this:const optionalA = (args.length > 0) ? args.shift() : null;
        const optionalB = (args.length > 0) ? args.shift() : null;
        // ... repeat for more itemsif (err && callback) return callback(err);

        /* do your thing */
    }

    // invoke example function with and without optional argumentsexample(null, 'AA');

    example(null, function (err) {   /* do something */    });

    example(null, 'AA', function (err) {});

    example(null, 'AAAA', 'BBBB', function (err) {});

Required Parameters: 必要参数

在 ECMAScript 5 中,如果你希望函数调用者填入一个必要参数,那么你可能会手动判断参数对象的长度等:

function foo(mustBeProvided) {
    if (arguments.length < 1) {
        throw new Error();
    }
    if (! (0 in arguments)) {
        throw new Error();
    }
    if (mustBeProvided === undefined) {
        throw new Error();
    }
    ···
}

而在 ECMAScript 6 中,你可以使用默认值机制,即将某个参数的默认值定义为抛出异常的函数,即如下:

/**
 * Called if a parameter is missing and
 * the default value is evaluated.
 */
function mandatory() {
    throw new Error('Missing parameter');
}
function foo(mustBeProvided = mandatory()) {
    return mustBeProvided;
}

Interaction:

> foo()
Error: Missing parameter
> foo(123)
123

Anonymous:匿名函数

在 ES5 中,可以用myFunction = function(){}的方式来声明一个匿名函数,而 ES6 中,可以用类似于 Lambda 的 Arrow 方式来声明一个匿名表达式。

// Expression bodies
const odds = evens.map((v) => v + 1);
const nums = evens.map((v, i) => v + i);
const pairs = evens.map((v) => ({ even: v, odd: v + 1 }));

// Statement bodies
nums.forEach((v) => {
  if (v % 5 === 0) fives.push(v);
});

不过不同于 ES5 中的以 function 关键字声明的方式,Arrow 方式是会与外层共享 this 指针的,其效果如下所示:

// Lexical this
const bob = {
  _name: "Bob",
  _friends: [],
  printFriends() {
    this._friends.forEach((f) => console.log(this._name + " knows " + f));
  },
};

Invoke( 函数调用 )

常见的Function调用方式即是```FunctionName(args)```,也可以利用apply与call方式进行调用。

apply&call

  • Function.apply(obj,args) 方法能接收两个参数

obj:这个对象将代替 Function 类里 this 对象

args:这个是数组,它将作为参数传给 Function(args–>arguments )

  • Function.call(obj,[param1[,param2[,…[,paramN]]]])

obj:这个对象将代替 Function 类里 this 对象

params:这个是一个参数列表

在 JavaScript 中函数也是对象,对象则有方法,apply 和 call 就是函数对象的方法。这两个方法异常强大,他们允许切换函数执行的上下文环境(context ),即 this 绑定的对象。很多 JavaScript 中的技巧以及类库都用到了该方法。让我们看一个具体的例子:

function Point(x, y){
    this.x = x;
    this.y = y;
    this.moveTo = function(x, y){
        this.x = x;
        this.y = y;
    }
 }

以Math.max为例

Math.max(arr[0],arr[1]); Math.prototype.max.call(Math,arr[0],arr[1]); Math.prototype.max.apply(Math,arr);

 const p1 = new Point(0, 0);
 const p2 = {x: 0, y: 0};
 p1.moveTo(1, 1);
 p1.moveTo.apply(p2, [10, 10]);

使用 call/apply 来用 Array 的方法处理 Array-Like 对象

笔者之前一直有个疑问,就是从面向对象的思路上来说,调用对象的某个方法应该是用 obj.function(),但是在 JS 的很多类库中,笔者是发现更多人愿意用 obj.Prototype.function.call(obj)这种方式,为此笔者还特意在Reddit上问了个问题,在这里把大概的解释如下。这种情况更多的是出现在对于非 Array 对象而类似于 Array 对象的 Object 上使用一系列 Array 的函数,譬如:

const items = {  length: 2,  0: 'bob',  1: 'joe' };
console.log(items.forEach); // => undefined
[].forEach.call(items, x => console.log(x)) // => bob => joe

而在具体的编程中,这种 Array-Like 的数组大概有如下几种情况:( 1)函数调用时的 arguments

// arguments.length is an integer, this doesn't leak the arguments object itself
const args = new Array(arguments.length);
for(const i = 0; i < args.length; ++i) {
   // i is always valid index in the arguments object, preventing another deoptimization.
   args[i] = arguments[i];
}

(2 )在 DOM 编程中的 NodeList 可以参考CodePen

console.clear();

const divs = document.getElementsByTagName("DIV");
console.dir(divs.__proto__); //HTMLCollection
//If it were an array, then proto would be "Array"

const arr = [1, 2, 3, 4];
console.dir(arr.__proto__); //Array[0]
//Yep.

//And the slice method won't even work with divs because the HTMLCollection prototype doesn't have that method

//divs.slice(); //Uncaught TypeError: divs.slice is not a function
//:(

//Now, through the magic of [].slice.call(), we can make divs an array...

divs = [].slice.call(divs);
console.dir(divs.__proto__); //Array[0]
//Bingo!

//Now...

const lastCouple = divs.slice(2);
console.dir(lastCouple); //Array[2]
//slice works now!

IIFE - Immediately Invoked Function Expression

Called as “Iffy” ( IIFE - immediately invoked function expression) is an anonymous function expression that is immediately invoked and has some important uses in Javascript.

(function() {
 // Do something
 }
)()

It is an anonymous function expression that is immediately invoked, and it has some particularly important uses in JavaScript.

The pair of parenthesis surrounding the anonymous function turns the anonymous function into a function expression or variable expression. So instead of a simple anonymous function in the global scope, or wherever it was defined, we now have an unnamed function expression.

Similarly, we can even create a named, immediately invoked function expression:

(someNamedFunction = function(msg) {
    console.log(msg || "Nothing for today !!")
    }) (); // Output --> Nothing for today !!

someNamedFunction("Javascript rocks !!"); // Output --> Javascript rocks !!
someNamedFunction(); // Output --> Nothing for today !!

不过 IIFE 最常见的用法就是希望能够将变量的作用范围限定在块作用域内,而在 ES6 中我们已经可以使用let关键字来创建块级别的作用域而不需要再使用这种 IIFE 的方式:

(function () {
    const food = 'Meow Mix';
}());

console.log(food); // Reference Error

使用 ES6 的块

{
    let food = 'Meow Mix';
}

console.log(food); // Reference Error

不使用括号调用函数

(1 )作为构造器调用利用new关键字可以不带括号地调用函数:

function Greet() {
  console.log("hello");
}
new Greet(); // parentheses are optional in this construct.

new操作符的语法为:

new constructor[([arguments])]

( 2)隐性实现 toString 或者 valueOf 的调用另一个例子就可以隐性调用toString或者valueOf方法

const greet = {
    toString: function() {
         return 'hello';
    }
}

greet + ''; // 字符串连接会强制性转化到String类型,这样就隐性调用了toString

可以利用这种方式调用任意的函数

function func() {
    console.log('hello');
}

const greet = {
    toString: func
}

greet + '';

或者使用 valueOf:

function func() {
    console.log('hello');
}

const greet = {
    valueOf: func
}

+greet;

如果要使用 valueOf 的话,可以在 Function 的原型中完成复写,这样也能完成一个函数的传递:

Function.prototype.valueOf = function() {
    this.call(this);
    // Optional improvement: avoid `NaN` issues when used in expressions.
    return 0;
};
function greet() {
    console.log('hello');
}

+greet;

( 3)Iterators 可以利用 * 操作符创建一个迭代器,然后在下一个元素被遍历的时候就会被自动调用了:

function* func() {
    console.log('hello');
}

const greet = {};
greet[Symbol.iterator] = func;

[...greet];

一般来说用迭代器的时候都会附带一个yield语句,但是在这边希望调用某个函数的时候不一定要加上这个语句。上述代码中是最后一个语句调用了函数,同时也可以利用解构赋值来进行调用

[,] = greet;

或者使用 for … of 结构

for ({} of greet);

(4 ) Getters

function func() {
    console.log('hello');
}

Object.defineProperty(window, 'greet', { get: func });

greet;

也可以利用Object.assign:

Object.assign(window, { get greet() {
    console.log('hello');
}});
greet;

全局将 window 对象替换成一个你自定义的全局对象。

  1. Tagged Template Literals

ES6 中可以利用模板字符串的方式调用:

function greet() {
    console.log('hello');
}

greet``;

Lambda:Arrow Function

Arrow Function 是 ES6 新增的特性,很类似于 Java 或者 C# 中的 Lambda 表达式。Arrow 函数中的 this 指针在创建时就被绑定到了闭合的作用域内,不会受到 new、bind、call 以及 apply 这些方法的影响。

const o = {
  traditionalFunc: function () {
    // Normal function, bound as expected
    console.log("traditionalFunc this === o?", this === o);
  },
  arrowFunc: () => {
    // Arrow function, bound to scope where it's created
    console.log("arrowFunc this === o?", this === o);
    console.log("arrowFunc this === window?", this === window);
  },
};

o.traditionalFunc();
// traditionalFunc this === o? true

o.arrowFunc();
// arrowFunc this === o? false
// arrowFunc this === window? true

上述代码中的 arrowFunc 隶属于 o 对象,但是在 window 的作用域中被创建,因此,最终 arrow 函数中的 this 指针的值等于 window 对象。ES5 中的对于 this 的控制已然非常复杂,特别是在处理异步代码中如何传入合适的 this 对象也是一件麻烦事,如下文所示

const asyncFunction = (param, callback) => {
  window.setTimeout(() => {
    callback(param);
  }, 1);
};

// With a traditional function if we don't control
// the context then can we lose control of `this`.
const o = {
  doSomething: function () {
    // Here we pass `o` into the async function,
    // expecting it back as `param`
    asyncFunction(o, function (param) {
      // We made a mistake of thinking `this` is
      // the instance of `o`.
      console.log("param === this?", param === this);
    });
  },
};

o.doSomething(); // param === this? false
上一页
下一页