函数
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.
// bad
function emailClients(clients) {
clients.forEach((client) => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
// good
function emailClients(clients) {
clients
.filter(isClientActive)
.forEach(email);
}
function isClientActive(client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}
- 保持简单。
基本特性
- 在 JS 之中,函数也和数组一样被视为一种对象,只不过是内置的对象,它是“第一公民”。
- 定义:上下文 (Context): 当通过一个对象来调用函数时,这个对象就是此次调用的上下文。
- 定义:构造函数 (Constructor):创建一个新的对象的函数。
// 函数的定义
// 标准式
function name (args) {}
// 变量式
var f_name = function (args) {};
// 递归式
var f = function fact (x) {if (x <= 1) return 1; else return x*fact(x-1)};
// 匿名式
array.sort(function(x,y) {return x-y;});
fnc.name;
fnc.length;
fnc.caller;
fnc.prototype.constructor;
fnc.arguments; // 并非真正的数组
arguments.callee // current calling function
// 函数的调用
// 直接调用 this 会被绑定到运行这个函数的上下文中
funcname();
// 作为方法调用 this 会被绑定到具有该方法的对象之上
obj.funcname();
// 作为构造函数调用 如果在一个函数前面加上new来调用,那么将创建一个隐藏连接到该
// 函数的prototype成员的新对象,同时this将会绑定到那个新对象上
var obj = new Funcname();
// call 和 apply this build 绑定到第一个参数声明的对象上
funcname.call(context, arg1, arg2, arg3, ...);
funcname.apply(context, [arg1, arg2, arg3 ...]);
funcname.bind(obj); // switch the running context to obj
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 ofthis
inside of the function is determined by where the arrow function is defined not where it is used. - Not
new
able - Arrow functions cannot be used as constructors and will throw an error when used withnew
. - Can’t change
this
- The value ofthis
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 thearguments
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)) {
// ...
}