读Secrets of the JavaScript Ninja(一)函数

理解JavaScript为什么应该作为函数式

在JavaScript中,函数是程序执行过程中的主要模块单元

函数是第一类对象

  • 通过字面量创建
1
function ninjaFunction(){}
  • 赋值给变量,数组项或其它对象的属性
1
2
3
var ninjaFunction = function() {}
ninjaFunction.push(function(){})
ninja.data = function(){}
  • 作为函数参数来传递
1
call(function(){})
  • 作为函数的返回值
1
2
3
function returnNewNinjaFunction() {
return function() {}
}
  • 具有动态创建和分配的属性
1
2
var ninjaFunction = function() {}
ninjaFunction.ninja = "Hanzo"

函数作为对象的乐趣

通过向函数添加属性来实现存储函数和自记忆函数

存储函数

1
2
3
4
5
6
7
8
9
10
var store = {
nextId: 1,
cache: {},
add: function(fn) {
if (!fn.id) {
fn.id = this.nextId++;
this.cache[fn.id] = fn;
return true;
}}
};

记忆函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function isPrime(value) {
if (!isPrime.answers) {
isPrime.answers = {};
}
if (isPrime.answers[value] !== undefined) {
return isPrime.answers[value];
}
var prime = value !== 0 && value !== 1;
for (var i = 2; i < value; i++) {
if (value % i === 0) {
prime = false;
break;
}
}
return isPrime.answers[value] = prime;
}

函数定义

  • 函数声明

在声明前就可以被调用

1
function myFun(){ return 1;}
  • 函数表达式

必须先声明

1
const func = function() {}
  • 箭头函数

函数内的this从上一层作用域继承

1
2
3
(myArg) => {
return myArg*2
}

如果括号里只有一个参数就可以省略括号,箭头后只有一条语句可以省略大括号,作为这个箭头函数的返回值

  • 立即函数

声明即调用

1
(function(){})(3)

函数的参数

剩余参数

剩余参数以…作为前缀声明,以数组形式传入函数

1
2
3
4
5
6
function multiMax(first, ...remainingNumbers) {
var sorted = remainingNumbers.sort(function(a, b) {
return b – a;
});
return first * sorted[0];
}

默认参数

1
2
3
function performAction(ninja, action = "skulking") {
return ninja + " " + action;
}

理解函数调用

隐含函数参数

函数调用时还会传递两个隐式的参数: arguments和this。
这些隐式参数在函数声明中没有明确定义, 但会默认传递给函数并
且可以在函数内正常访问。

arguments参数

arguments可以访问到传给函数所有的参数,虽然arguments具有Length属性,但是它只是一个类数组结构,并不是数组

  • 操作所有参数
1
2
3
4
5
6
7
function sum() {
var sum = 0;
for(var i = 0; i < arguments.length; i++){
sum += arguments[i];
}
return sum;
}

this参数: 函数上下文

this参数的指向不仅是由定义函数的方式和位置决定的, 同时还严重受到函数调用方式的影响。

函数调用

四种调用函数的方法

  • 作为一个函数(function)——skulk(), 直接被调用。
  • 作为一个方法(method)——ninja.skulk(), 关联在一个对象上, 实现面向对象编程。
  • 作为一个构造函数(constructor)——new Ninja(), 实例化一个新的对象。
  • 通过函数的apply或者call方法——skulk.apply(ninja)或者
    skulk.call(ninja)

1. 作为函数直接调用

也就是在全局环境中直接调用,在严格模式下,函数内部的this应该是undefined,非严格模式下就是window

1
2
3
4
5
function ninja() {};
ninja();
var samurai = function(){};
samurai();
(function(){})()

2. 作为方法被调用

当作为方法被调用时,函数内部的this应该为调用这个函数的对象

1
2
3
var ninja = {};
ninja.skulk = function(){};
ninja.skulk();

3. 作为构造函数调用

当函数时候new关键词作为构造函数来调用时,会发生三个动作

  1. 创建一个空对象
  2. 把这个空对象作为当前函数的this
  3. 新构造的对象作为new的返回值
1
2
3
4
5
6
7
function Ninja() {
this.skulk = function() {
return this;
};
}
var ninja1 = new Ninja();
var ninja2 = new Ninja();

4. 使用apply和call方法调用

JavaScript为我们提供了一种调用函数的方式, 从而可以显式地指定任何对象作为函数的上下文。 apply、call是函数的方法,即函数在JavaScript中也是对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function juggle() {
var result = 0;
for (var n = 0; n < arguments.length; n++) {
result += arguments[n];
}
this.result = result;
}
var ninja1 = {};
var ninja2 = {};
juggle.apply(ninja1,[1,2,3,4]);
juggle.call(ninja2, 5,6,7,8);

assert(ninja1.result === 10, "juggled via apply");
assert(ninja2.result === 26, "juggled via call");

5. 使用箭头函数绕过函数上下文

箭头函数的this也就是函数上下文是从它的上层作用域继承来的

1
2
3
this.click = () => {
this.clicked = true;
};

6. 使用bind函数

bind也是函数原型的一个方法, 为一个函数绑定它的上下文

1
var boundFunction = button.click.bind(button);

闭包和作用域

什么是闭包

闭包允许函数访问并操作函数外部的变量。只要变量或函数存在于
声明函数时的作用域内,闭包即可使函数能够访问这些变量或函数

1
2
3
4
5
6
7
8
9
10
11
12
var outerValue = "samurai";
var later;
function outerFunction() {
var innerValue = "ninja";
function innerFunction() {
assert(outerValue === "samurai", "I can see the samurai.");
assert(innerValue === "ninja", "I can see the ninja.")
}
later = innerFunction;
}
outerFunction();
later();

当在外部函数中声明内部函数时, 不仅定义了函数的声明, 而且还创建了一个闭包。 该闭包不仅包含了函数的声明, 还包含了在函数声明时该作用域中的所有变量。 当最终执行内部函数时, 尽管声明时的作用域已经消失了, 但是通过闭包, 仍然能够访问到原始作用域

使用闭包

通过构造函数创建了一个含有两个方法getFeints,feint的对象,函数内部的feints变量只有通过getFeints实现了闭包才能访问,从而实现了封装私有变量

封装私有变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Ninja() {  
var feints = 0;
this.getFeints = function() {
return feints;
};
this.feint = function() {
feints++;
};
}
var ninja1 = new Ninja();
ninja1.feint();
assert(ninja1.feints === undefined,
"And the private data is inaccessible to us.");
assert(ninja1.getFeints() === 1,
"We're able to access the internal feint count.");
var ninja2 = new Ninja();
assert(ninja2.getFeints() === 0,
"The second ninja object gets its own feints variable.");

回调函数

处理回调函数是另一种常见的使用闭包的情景。 回调函数指的是需
要在将来不确定的某一时刻异步调用的函数。 通常,在这种回调函数
中, 我们经常需要频繁地访问外部数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function animateIt(elementId) {
var elem = document.getElementById(elementId);
var tick = 0;
var timer = setInterval(function() {
if (tick < 100) {
elem.style.left = elem.style.top = tick + "px";
tick++;
}
else {
clearInterval(timer);
assert(tick === 100,
"Tick accessed via a closure.");
assert(elem,
"Element also accessed via a closure.");
assert(timer,
"Timer reference also obtained via a closure.");
}
}, 10);
}
animateIt("box1");

未来的函数: 生成器和promise

使用生成器函数

生成器函数几乎是一个完全崭新的函数类型, 它和标准的普通函数
完全不同。生成器(generator) 函数能生成一组值的序列, 但每个值的生成是基于每次请求, 并不同于标准函数那样立即生成。 我们必须显式地向生成器请求一个新的值, 随后生成器要么响应一个新生成的值, 要么就告诉我们它之后都不会再生成新值。 更让人好奇的是, 每当生成器函数生成了一个值, 它都不会像普通函数一样停止执行。 相反, 生成器几乎从不挂起。 随后, 当对另一个值的请求到来后, 生成器就会从上次离开的位置恢复执行。

1
2
3
4
5
6
7
8
function* WeaponGenerator() {  
yield "Katana";
yield "Wakizashi";
yield "Kusarigama";
}
for (let weapon of WeaponGenerator()) {
assert(weapon !== undefined, weapon);
}

通过迭代器对象控制生成器

调用生成器函数不一定会执行生成器函数体。 通过创建迭代器对
象, 可以与生成器通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function* WeaponGenerator() {
yield "Katana";
yield "Wakizashi";
}
const weaponsIterator = WeaponGenerator();
const result1 = weaponsIterator.next();
assert(typeof result1 === "object"
&& result1.value === "Katana"
&& !result1.done,
"Katana received!");
const result2 = weaponsIterator.next();
assert(typeof result2 === "object"
&& result2.value === "Wakizashi"
&& !result2.done,
"Wakizashi received!");
const result3 = weaponsIterator.next();
assert(typeof result3 === "object"
&& result3.value === undefined
&& result3.done,
"There are no more results!");

通过调用生成器得到的迭代器, 暴露出一个next方法能让我们向生
成器请求一个新值。 next方法返回一个携带着生成值的对象, 而该对象中包含的另一个属性done也向我们指示了生成器是否还会追加生成值。

1
2
3
4
5
6
7
8
9
function* WeaponGenerator(){
yield "Katana";
yield "Wakizashi";
}
const weaponsIterator = WeaponGenerator();
let item;
while(!(item = weaponsIterator.next()).done) {
assert(item !== null, item.value);
}

把执行权交给下一个生成器

1
2
3
4
5
6
7
8
9
10
11
12
function* WarriorGenerator(){
yield "Sun Tzu";
yield* NinjaGenerator();
yield "Genghis Khan";
}
function* NinjaGenerator(){
yield "Hattori";
yield "Yoshi";
}
for(let warrior of WarriorGenerator()){
assert(warrior !== null, warrior);
}

作为生成器函数参数发送值

1
2
3
4
5
6
7
8
9
10
11
12
function* NinjaGenerator(action) {
const imposter = yield ("Hattori " + action);
assert(imposter === "Hanzo",
"The generator has been infiltrated");
yield ("Yoshi (" + imposter + ") " + action);
}
const ninjaIterator = NinjaGenerator("skulk");
const result1 = ninjaIterator.next();
assert(result1.value === "Hattori skulk","Hattori is skulking");
const result2 = ninjaIterator.next("Hanzo");
assert(result2.value === "Yoshi (Hanzo) skulk",
"We have an imposter!");

生成器内部构成

  • 挂起开始——创建了一个生成器后, 它最先以这种状态开始。 其中
    的任何代码都未执行。
  • 执行——生成器中的代码执行的状态。 执行要么是刚开始, 要么是
    从上次挂起的时候继续的。 当生成器对应的迭代器调用了next方
    法, 并且当前存在可执行的代码时, 生成器都会转移到这个状态。
  • 挂起让渡——当生成器在执行过程中遇到了一个yield表达式, 它会
    创建一个包含着返回值的新对象, 随后再挂起执行。 生成器在这个
    状态暂停并等待继续执行。
  • 完成——在生成器执行期间, 如果代码执行到return语句或者全部代码执行完毕, 生成器就进入该状态

使用promise

promise是ES6中引入来解决异步人物的一个方案,promise对象是对我们现在尚未得到但将来会得到值的占位符;它是对我们最终能够得知异步计算结果的一种保证。

1
2
3
4
5
6
7
8
9
const ninjaPromise = new Promise((resolve, reject) => {
resolve("Hattori");
//reject("An error resolving a promise!");
});
ninjaPromise.then(ninja => {
assert(ninja === "Hattori", "We were promised Hattori!");
},err => {
fail("There shouldn't be an error")
});

resolve, reject

resolve为异步任务成功后调用下一个then

1
2
3
4
5
6
7
8
const ninjaImmediatePromise = new Promise((resolve, reject) => {
report("ninjaImmediatePromise executor. Immediate resolve.");
resolve("Yoshi");
});
ninjaImmediatePromise.then(ninja => {
assert(ninja === "Yoshi",
"ninjaImmediatePromise resolve handled with Yoshi");
});

reject为失败抛出异常,由catch接住

1
2
3
4
5
const promise = new Promise((resolve, reject) => {
reject("Explicitly reject a promise!");
});
promise.then(() => fail("Happy path, won't be called!"), error => pass("A promise was explicitly rejected!")
);

把生成器和promise相结合

将生成器和promise结合, 从而以优雅的同步代码方式完成异步任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
async(function*(){  
try {
const ninjas = yield getJSON("data/ninjas.json");
const missions = yield getJSON(ninjas[0].missionsUrl);
const missionDescription = yield getJSON(missions[0].detailsUrl);
}
catch(e) {
}
});
function async(generator) {
var iterator = generator();
function handle(iteratorResult) {
if(iteratorResult.done) { return; }
const iteratorValue = iteratorResult.value;
if(iteratorValue instanceof Promise) {
iteratorValue.then(res => handle(iterator.next(res)))
.catch(err => iterator.throw(err));
}
}

try {
handle(iterator.next());
}
catch (e) { iterator.throw(e); }
}

async函数获取了一个生成器, 调用它并创建了一个迭代器用来恢
复生成器的执行。 在async函数内, 我们声明了一个handle函数用于处理从生成器中返回的值——迭代器的一次“迭代”。 如果生成器的结果是一个被成功兑现的承诺,我们就是用迭代器的next方法把承诺的值返回给生成器并恢复执行。如果出现错误, 承诺被违背, 我们就使用迭代器的throw方法(告诉过你迟早能派上用场了) 抛出一个异常。 直到生成器的工作完成前, 我们都会一直重复这几个操作

实现同样功能的async函数

通过在关键字function之前使用关键字async, 可以表明当前的函数依赖一个异步返回的值。 在每个调用异步任务的位置上, 都要放置一个await关键字, 用来告诉JavaScript引擎, 请在不阻塞应用执行的情况下在这个位置上等待执行结果。

1
2
3
4
5
6
7
8
9
10
(async function () {
try {
const ninjas = await getJSON("data/ninjas.json");
const missions = await getJSON(missions[0].missionsUrl);
console.log(missions);
}c
atch(e){
console.log("Error: ", e);
}
})()