函数

JavaScript 函数编写通常应该遵循如下的最佳实践:

  • 不要重复造轮子,将复用性质的逻辑统一封装为:函数、对象和模块。
  • 单一职责原则:一次只做一件事情。When you have more than one level of abstraction your function is usually doing too much. Splitting up functions leads to reusability and easier testing.
  1. // bad
  2. function emailClients(clients) {
  3. clients.forEach((client) => {
  4. const clientRecord = database.lookup(client);
  5. if (clientRecord.isActive()) {
  6. email(client);
  7. }
  8. });
  9. }
  10. // good
  11. function emailClients(clients) {
  12. clients
  13. .filter(isClientActive)
  14. .forEach(email);
  15. }
  16. function isClientActive(client) {
  17. const clientRecord = database.lookup(client);
  18. return clientRecord.isActive();
  19. }
  • 保持简单。

基本特性

  • 在 JS 之中,函数也和数组一样被视为一种对象,只不过是内置的对象,它是“第一公民”。
  • 定义:上下文 (Context): 当通过一个对象来调用函数时,这个对象就是此次调用的上下文。
  • 定义:构造函数 (Constructor):创建一个新的对象的函数。
  1. // 函数的定义
  2. // 标准式
  3. function name (args) {}
  4. // 变量式
  5. var f_name = function (args) {};
  6. // 递归式
  7. var f = function fact (x) {if (x <= 1) return 1; else return x*fact(x-1)};
  8. // 匿名式
  9. array.sort(function(x,y) {return x-y;});
  10. fnc.name;
  11. fnc.length;
  12. fnc.caller;
  13. fnc.prototype.constructor;
  14. fnc.arguments; // 并非真正的数组
  15. arguments.callee // current calling function
  16. // 函数的调用
  17. // 直接调用 this 会被绑定到运行这个函数的上下文中
  18. funcname();
  19. // 作为方法调用 this 会被绑定到具有该方法的对象之上
  20. obj.funcname();
  21. // 作为构造函数调用 如果在一个函数前面加上new来调用,那么将创建一个隐藏连接到该
  22. // 函数的prototype成员的新对象,同时this将会绑定到那个新对象上
  23. var obj = new Funcname();
  24. // call 和 apply this build 绑定到第一个参数声明的对象上
  25. funcname.call(context, arg1, arg2, arg3, ...);
  26. funcname.apply(context, [arg1, arg2, arg3 ...]);
  27. funcname.bind(obj); // switch the running context to obj
  28. this::funcName();

Notice:

  • 函数是JavaScript的第一类对象 first-class object,可以作为带有属性和方法的值以及参数进行传递
  • 函数提供了局部作用域,而其大括号不能够提供这种局部作用域,声明的局部变量会被提升到局部作用域的顶部
  • 函数在 eval() with() 以及 try ... catch ... 内是块级作用域
  • 在每个函数创建的时候有两个附件的隐藏属性:函数的上下文和实现函数行为的代码
  • 通过函数字面量创建的函数对象包含一个连接到外部的上下文的连接。这叫做 闭包
  • 匿名自执行函数 IIFEs (Immediately Invoked Function Expressions),但最佳的实践还是传入非匿名的函数。
  • arguments 伪数组

this 和对象原型

this 是在运行的时候绑定的,它的上下文取决于函数执行的时候的种种条件。this 的绑定和函数的声明位置并没有任何关系,只取决于函数的 调用方式

当一个函数被调用的时候,解释器会为其创建一个执行上下文(context)这个上下文里会包含:调用栈、函数的调用方法、传输参数等信息,this 就是其中的一个属性。

调用栈中的调用位置,决定了 this 的绑定。

  • 默认绑定:独立函数的调用会将 this 绑定到全局对象上。在严格模式下,this 会被赋值为 undefined
  • 隐式绑定:当函数引用有上下文对象的时候,隐式绑定会将函数中 this 的引用指向该对象。

    • 对象属性的引用中,只有最顶层,或者说调用的最后一层影响调用位置。obj2.obj1.foo() 此时 foo 中的 this 绑定到 obj1 上。
    • 隐式丢失情况,见后方 code
  • 显式绑定:apply() call() 以及一些 API 调用的上下文
  • bind()apply() / call() 类型。
  • new 绑定:构造函数会将 this 绑定到新建的对象之上
// 隐式丢失
var foo = function() {console.log(this.a)}
var obj = {a: 42, foo: foo}
var bar = obj.foo
var a = 'fool!'
bar() // output: fool!

四种绑定的优先级:显式声明 > new > 隐式声明 > 默认绑定


  • Nested Functions 嵌套函数

    • This provides a great deal of utility in writing more maintainable code. If a function relies on one or two other functions that are not useful to any other part of your code, you can nest those utility functions inside the function that will be called from elsewhere. This keeps the number of functions that are in the global scope down, which is always a good thing.
  • Lambdas 演算:函数返回函数

    • Curry 柯里化(高阶函数)
  • Callbacks 回调函数
  • 链式调用 / 流式调用
  • 记忆函数

闭包

函数能够记住并访问所在的词法作用域(作用域链),即使函数是在当前作用域之外执行,这时也就产生了闭包。

定义:A closure is a function that captures the external bindings (i.e., not its own arguments) contained in the scope in which it was defined for later use (even after that scope has completed).

闭包因为包含了其它函数的作用域,所以往往会占据更多的内存。过度地使用可能导致内存占用过多。

JavaScript 作用域

深入理解作用域系列:

  • 全局作用域 Global Scope:这个容易理解,JS 的顶层作用域。
  • 函数作用域 Function Scope:每个函数在运行的时候都会创建自己的作用域(传入参数、内部提升的变量)
  • 词法作用域 Lexical Scope:每个函数都会保存指向上级作用域的作用域链(Scope Chain),这个作用域链一直延伸到全局作用域。每次变量查找就会沿着作用域链进行查找。

    • eval()with() 为欺骗词法,应该尽量不用
  • 动态作用域 Dynamic Scope:JS 在执行 this 的上下文绑定的时候,使用动态作用域,动态作用域和 call 以及 apply 还有 bind 相关,同时是在执行的时候赋值的,所以称之为动态的。
  • 块级作用域 Block Scope:如同 Java / C++ 中 {} 内的变量作用域,在 ES6 中,let 可以创建块级作用域。

其它关注点:

  • 模型 Models: Complier | Engine | Scope | Environment

    • Whenever JavaScript executes a function, a ‘scope’ object is created to hold the local variables created within that function. It is initialized with any variables passed in as function parameters.

      • a brand new scope object is created every time a function starts executing.
      • these scope objects cannot be directly accessed from your JavaScript code.
    • Scope objects form a chain called the scope chain, similar to the prototype chain used by JavaScript’s object system.
  • 嵌套作用域

    • 作用域链 Scope Chain

      • 暗喻 Metaphors | 楼房以及包含图
    • 每个嵌套作用域包括

      • 形参 | { } 内的所有声明变量
    • 匿名函数:匿名函数最好被声明函数名称

      • 利于Debug | 利于复用 | 利于callee调用
      • callee is a property of the arguments object. It can be used to refer to the currently executing function inside the function body of that function.
      • caller is used to find which function calls this function.
    • IIAF 匿名立即执行函数:对块级作用域的一种模仿。

      • 减少全局变量污染
      • 但是使用闭包的时候需要小心,因为容易导致:内存泄露

Functional JavaScript

请参考:functional_javascript.md

ES6 function

Default Parameters

function makeRequest(url, timeout=2000, callback=()=>{}) {
    // the rest of the function
}

Rest Parameters:...xxx, xxx is the real array.

function sum(first, ...numbers) {
    let result = first,
        i = 0,
        len = numbers.length;
    while (i < len) { result += numbers[i]; i++; }
    return result;
}
sum(...[1, 2, 3]) // sum(1, 2, 3)

Destructed Parameters

function setCookie(name, value, { secure, path, domain, expires }) {
    // ...
}
setCookie("type", "js", { secure: true, expires: 60000 });

The Spread Operator

let values = [-25, -50, -75, -100]
console.log(Math.max(...values, 0));        // 0

The name property: function get a name property.

Block-level Functions

"use strict";
if (true) {
    console.log(typeof doSomething);        // "function"
    console.log(typeof getsomething);       // Error: not defined
    function doSomething() {
        // ...
    }
    let getsomething = function () {};
    doSomething();
}
console.log(typeof doSomething);            // "undefined"

Arrow Function

  • Lexical this binding - The value of this inside of the function is determined by where the arrow function is defined not where it is used.
  • Not newable - Arrow functions cannot be used as constructors and will throw an error when used with new.
  • Can’t change this - The value of this inside of the function can’t be changed, it remains the same value throughout the entire lifecycle of the function.
  • No arguments object - You can’t access arguments through the arguments object, you must use named arguments or other ES6 features such as rest arguments.
var reflect = value => value;
// effectively equivalent to:
var reflect = function(value) {
    return value;
};

var sum = (num1, num2) => num1 + num2;
// effectively equivalent to:
var sum = function(num1, num2) {
    return num1 + num2;
};

var getName = () => "Nicholas";
// effectively equivalent to:
var getName = function() {
    return "Nicholas";
};

var PageHandler = {
    id: "123456",
    init: function() {
        document.addEventListener("click",
            event => this.doSomething(event.type), false);
    },
    doSomething: function(type) {
        console.log("Handling " + type  + " for " + this.id);
    }
};

IIFEs

let person = (function(name) {
    return {
        getName() {
            return name;
        }
    };
}("Nicholas"));
console.log(person.getName());      // "Nicholas"

Lexical this Binding

var PageHandler = {
    id: "123456",
    init: function() {
        document.addEventListener("click", (function(event) {
            this.doSomething(event.type);     // no error
        }).bind(this), false);
    },
    doSomething: function(type) {
        console.log("Handling " + type  + " for " + this.id);
    }
};

Tail Calls

Calls in tail-position are guaranteed to not grow the stack unboundedly. Makes recursive algorithms safe in the face of unbounded inputs.

Best Practice

Don’t use flags as function parameters.

// Bad
function createFile(name, temp) {
  if (temp) {
    fs.create(`./temp/${name}`);
  } else {
    fs.create(name);
  }
}

// Good
function createFile(name) {
  fs.create(name);
}

function createTempFile(name) {
  createFile(`./temp/${name}`);
}

Avoid Side-Effects.

// Bad
const addItemToCart = (cart, item) => {
  cart.push({ item, date: Date.now() });
};

// Good
const addItemToCart = (cart, item) => {
  return [...cart, { item, date : Date.now() }];
};

Functional languages are cleaner and easier to test, do it in functional way.

const totalOutput = programmerOutput
  .map((programmer) => programmer.linesOfCode)
  .reduce((acc, linesOfCode) => acc + linesOfCode, INITIAL_VALUE);

Encapsulate condition expressions.

function shouldShowSpinner(fsm, listNode) {
  return fsm.state === 'fetching' && isEmpty(listNode);
}

if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
  // ...
}