函数声明

函数声明与闭包

参数

arguments

JavaScript的函数里面的arguments对象有.lengh属性和可以通过[]访问,但是确实从Object.prototype继承的。很多时候都会用Array.prototype.slice.call(arguments)或者Array.from(arguments)转成数组。Brendan Eich本人也承认arguments的设计是因为当时只花了十天所以整得太糙了。在正式规范化JavaScript的时候,Microsoft曾经有人提出把arguments改成真正的ArrayBE本人甚至都打算动手改实现了,但是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改成Arraysubclass,但是很快就不了了之,只是在strict mode下对arguments.callee和上面的绑定quirk进行了限制。直到ES6终于对arguments提供了一个替代品- rest parameters:

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

BE本人并没有提到为什么一开始会把arguments设计成对象,因此我们也只能做猜测。但一个合理的推测是,ES1里面的Array.prototype其实很弱,只有四个方法:toString, join, reversesort -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]

默认参数与可选参数

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

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中,可以用类似于LambdaArrow方式来声明一个匿名表达式。

// 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中函数也是对象,对象则有方法,applycall就是函数对象的方法。这两个方法异常强大,他们允许切换函数执行的上下文环境(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;

或者使用forof结构

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 FunctionES6新增的特性,很类似于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
上一页
下一页