欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

读Secrets of the JavaScript Ninja(一)函数

程序员文章站 2022-06-26 12:05:30
...

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

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

函数是第一类对象

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

函数作为对象的乐趣

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

存储函数

var store = {
  nextId: 1,
  cache: {},
  add: function(fn) {
  if (!fn.id) {
  fn.id = this.nextId++;
  this.cache[fn.id] = fn;
  return true;
  }}
};

记忆函数

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;
}

函数定义

  • 函数声明

在声明前就可以被调用

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

必须先声明

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

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

(myArg) => {
  return myArg*2
}

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

  • 立即函数

声明即调用

(function(){})(3)

函数的参数

剩余参数

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

function multiMax(first, ...remainingNumbers) {
  var sorted = remainingNumbers.sort(function(a, b) {
  return b – a;
});
  return first * sorted[0];
}

默认参数

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

理解函数调用

隐含函数参数

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

arguments参数

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

  • 操作所有参数
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

function ninja() {};
ninja();
var samurai = function(){};
samurai();  
(function(){})()

2. 作为方法被调用

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

var ninja = {};
ninja.skulk = function(){};
ninja.skulk();

3. 作为构造函数调用

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

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

4. 使用apply和call方法调用

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

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也就是函数上下文是从它的上层作用域继承来的

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

6. 使用bind函数

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

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

闭包和作用域

什么是闭包

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

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实现了闭包才能访问,从而实现了封装私有变量

封装私有变量

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.");

回调函数

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

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

function* WeaponGenerator() {  
  yield "Katana";
  yield "Wakizashi";
  yield "Kusarigama";  
}
for (let weapon of WeaponGenerator()) {
  assert(weapon !== undefined, weapon);
}

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

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

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也向我们指示了生成器是否还会追加生成值。

function* WeaponGenerator(){
  yield "Katana";
  yield "Wakizashi";
}
const weaponsIterator = WeaponGenerator();  
let item;
while(!(item = weaponsIterator.next()).done) {
  assert(item !== null, item.value);
}

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

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);
}

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

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对象是对我们现在尚未得到但将来会得到值的占位符;它是对我们最终能够得知异步计算结果的一种保证。

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

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接住

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结合, 从而以优雅的同步代码方式完成异步任务

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引擎, 请在不阻塞应用执行的情况下在这个位置上等待执行结果。

(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);
  }
})()