JavaScript分析

169 阅读30分钟

第一部分 热身

Chapter1:理解JavaScript

与C#和java不同,JavaScript拥有更加纯正的函数式语言。 这些根本的差异包括以下内容

  1. 函数是一等公民(一等对象)——在JavaScript中,函数与其他对象共存,并且也能使用对象的功能。函数可以通过字面量创建,可以复制给变量。 ( 第一类对象的定义:他不一定是对象,他可以是一个实体, 他可以被存入变量或其他结构,可以被作为参参数传递给其他函数,可以作为函数的返回值,可以在执行器创造,而无需完全在设计期写出,即使没有被系解某一名称亦可以存在。 )
  2. 函数闭包
  3. 作用域
  4. 基于原型的面向对象

Chapter2:运行时的页面构建过程

2.1 生命周期概览

从序号四到序号七是生命周期流程 执行步骤:

  1. 页面构建——创建用户界面
  2. 事件处理——进入事件队列(5)等待用户交互产生(6),产生后执行传入交互事件的回调。
  3. 页面关闭——生命周期结束

2.2 页面构建阶段

在这里插入图片描述

建立Web应用UI得两个步骤

  • 解析HTML代码并构建文档对象模型(DOM)
  • 执行JavaScript代码 步骤一会在浏览器处理HTML节点的过程执行,步骤二会在解析到脚本节点执行,页面构建中,这两个步骤会交叉多次执行。

2.2.1 HTML解析和DOM构建

页面构建阶段创始于浏览器接受html时,该阶段为浏览器构建页面UI的基础。通过解析接收到的HTML代码,构建一个个HTML元素,构建DOM直至遇到第一个脚本元素。 image.png

2.2.2 执行JavaScript代码

所有包含在脚本元素中的代码由浏览器的JavaScript引擎执行,例如,FireFox的Spidermonkey引擎,Chrome和Opera的V8引擎和edge的Chakra引擎。浏览器通过一个API使JavaScript引擎可以与之交互并改变页面内容

JavaScript中的全局对象

浏览器暴露给JavaScript引擎的主要全局对象是window对象。其中最重要的就是document属性他代表了当前页面的dom。我们可以通过document来实现dom操作。 ![image.png](https://p9-.byteimg.com/tos-cn-i-k3u1fbpfcp/d165102bba8e4780a1685c6eccef27c7~tplv-k3u1fbpfcp-watermark.image?)

JavaScript代码的不同类型

大致有两种:一种是全局代码,一种是函数代码

      //函数代码中指的是函数中的代码
      function addMessage(element, message) {
        const messageElement = document.createElement("li");
        messageElement.textContent = message;
        element.appendChild(messageElement);
      }

      //全局代码指的是位于函数之外的代码
      const first = document.getElementById("first");
      addMessage(first, "pageloading");

全局代码由JavaScript引擎以一种自动的方式进行执行,而函数代码则需要被其他代码调用才能执行。当script元素的代码执行到最后一行,浏览器则会退出JavaScript执行模式,并开始为余下的dom构建节点。

2.3事件处理

客户端是一种GUI应用,也就是说这种应用会对不同的事件类型做出响应。因此在页面执行JavaScript时,除了会影响全局应用状态和修改DOM外,还会注册事件监听器。(值得注意的是每个script元素中的变量,都可以被同一个页面的其他script元素访问,因为他们共存于整个页面的生命周期)

2.3.1 事件处理器概览

image.png 流程:

  • 浏览器检查事件队列头
  • 如果浏览器没有在队列中检测到事件,则继续检查。
  • 如果检测出事件则去除该事件并执行相应的事件处理器。余下的事件则在队列中等待,直到他们上一个一个事件被处理。

(事件队列是在页面构建阶段和事件处理阶段以外的)

事件是异步的

因为事件难以估测预计的事件和顺序发生,所以他们应该被异步处理。

2.3.2 处理事件

这是代码:

document.body.addEventListener("mousemove", () => {

  //鼠标移动事件
  document.body.addEventListener("mousemove", function () {
    var second = document.getElementById("second");
    addMessage(second, "Event: mousemove");
  });
  
  //鼠标点击事件
  document.body.addEventListener("click", function () {
    var second = document.getElementById("second");
    addMessage(second, "Event: click");
  });
});

这是执行的过程:

image.png

第二部分 理解函数

chapter3 定义与参数

3.1 函数式的不同点是什么

除了全局代码,我们编写的所有脚本代码都将在一个函数内执行。函数是第一类对象,这会让我们的函数书写的更加自由。

3.1.2 回调函数

什么是回调函数呢,我们建立的任意函数会被其它函数在某个合适的时间点以参数的方式拿过去调用。

3.2函数作为对象的乐趣

  • 在集合中存储函数使我们可以轻易管理相关联的函数。例如某些情况下必须调用的回调函数。

示例:我们可以让做一个保存回调函数的集合,但是这会带来一个问题,我们添加进集合的回调函数可能会和集合中已有的函数重合,接下来我们将创建一个可以解决这个问题的方法

const store = {
  // 这个属性用来跟踪下一个被复制的函数
  nextId: 1,
  // 使用一个对象作为缓存,我们可以在其中存储函数
  cache: {},
  // 仅当函数唯一时,将该函数加入缓存
  add: function (fn) {
    // 判断添加的函数手否有id属性,如果有则证明已经添加过,静止添加
    if (!fn.id) {
      /* 
      如果没有则给准备添加进缓存的函数添加一个id值,并让自身的对象上的nextid下推一个,
      以追踪下一个装备添加进缓存的函数
      */
      fn.id = this.nextId++;
      // 在缓存中添加一个位置给准备加入缓存的函数
      this.cache[fn.id] = fn;
      // 表示添加成功
      return true;
    }
  },
};
  • 记忆能让函数记住上次计算得到的值,从而提高后续调用的性能。

    如同前面提到的,记忆化(memoization)是构建函数其中的一道过程,他可以记住上一次的计算结果。


function isPrime(value) {
  // 确认isprime函数的answers属性是否存在,如果不存在则新加一个(用来保存判断的结果)
  if (!isPrime.answers) {
    isPrime.answers = {};
  }
  
  // 查看这个缓存是否存在如果存在则直接返回之前保存的结果
  if (isPrime.answers[value] !== undefined) {
    return isPrime.answers[value];
  }
  
  // 添加判断条件,1 is not a prime
  let prime = value !== 0 && value !== 1;
  
  // 检查是否是质数
  for (var i = 2; i < value; i++) {
    if (value % i === 0) {
      prime = false;
      break;
    }
  }
  
  // 将判断结果赋值给answers数组
  return (isPrime.answers[value] = prime);
}
isPrime(5);

// output:71 { '5': true }
// 这个缓存是函数自身的一个属性,所以只要函数还存在,缓存也就存在
console.log(71, isPrime.answers);

这样写的优点: 由于函数调用时会寻找之前调用的值,所以用户最终会乐于看到所获得的性能收益。 他不需要额外的特殊请求,也不需要初始化就能顺利完成工作

缺点:

  1. 任何类型的缓存都必然会为性能牺牲内存
  2. 缓存逻辑不应该和业务逻辑混合,函数和方法只应该把一件事情做好。
  3. 对于这类问题很难估计算法复杂度,因为依赖于之前的结果输入。

3.3 函数定义

  • 函数定义(function declaration)和函数表达式(function express)最常用
  • 箭头函数(通常被叫做lamba函数)——ES6新增的JavaScript标准。能让我们以尽量简洁的语法定义函数
  • 函数构造函数-一种不常使用的函数定义方式。能让我们以字符串形式动态构造一个函数
new Function( 'a', 'b', 'return a + b' )
  • 生成器函数

3.4 函数声明和函数表达式

值得注意的是,正是因为JavaScript中的函数是第一类对象。这就意味着他们可以通过字面量创建,也可以复制给变量和属性

// 独立的函数申明
function myFunctionDeclaration() {
  // 内部函数声明
  function innerFunction() {}
}

// 函数表达式作为变量声明赋值语句中的一部分
let myFunc = function () {};
myFunc(function () {
  // 函数表达式作为函数返回值
  return function () {};
});

// 作为函数调用的一部分,命名函数表达式会被立即调用
(function namedFunctionExpression() {})();

// 函数表达式可以作为一元操作符的参数立即调用
+(function () {})(); //也可以这样写:+function() {} ()
-(function () {})();
!(function () {})();
~(function () {})();

// 函数声明和函数表达式的一个比较重要的不同点就是:对于函数声明来说,函数名是强制性的,而对于函数表达式来说,则是相反的
// 立即执行函数(function (param) {})(param)在后面这个函数做详细介绍,暂时先只介绍写法

/**
 * 箭头函数
 * 由于JavaScript中会使用大量函数,增加简化创建函数方法的语法也十分有意义。
 */
// 定义箭头函数
let greet = (name) => "Greetings" + name;

// 定义函数表达式
let greet1 = function (name) {
  return "Greeting" + name;
};

chapter4 :函数进阶:理解函数调用

4.1 使用隐式函数参数

在函数调用时,还会传递两个隐式参数:arguments和this;

4.1.1arguments参数

arguments参数是传递给函数所有参数的集合。无论是否有定义对应的形参,我们都可以通过他访问到函数的所有参数。借此可以实现原生的JavaScript并不支持的函数重载特性。但其实一般有了剩余参数的功能,arguments的使用空间已经大打折扣。但是处理老代码时可能还是会要用到。

// 使用arguments参数
function whatever(a, b, c) {
  // 检测传入的值是否准确
  console.log("a会等于1吗", a === 1);
  console.log("a会等于2吗", b === 2);
  console.log("a会等于3吗", c === 3);

  // 检测传入的参数个数是否为5
  console.log("参入的参数是否有5个", arguments.length === 5);

  //虽然他有length是属性,但他不是数组,也无法使用数组的方法
}

// arguments对象作为函数参数的别名
function infiltrate(person) {
  // 检测传值是否成功被接收
  console.log("传入的参数是否与相对应的形参符合", person === "gardener");
  console.log(
    "传入的参数是否被保存在了arguments对象中",
    arguments[0] === "gardener"
  );

  // 改变arguments的对象
  arguments[0] = "ninja";

  // 检测是否修改成功
  console.log("将arguments中的值进行修改成", arguments[0]);
  // 发现person的值也发生了改变
  console.log("person的值是", person);
}
infiltrate("gardener");

//避免使用别名,他会影响代码的可读性,在严格模式中就算改变了argument的值参数也不会改变了

4.1.2 this参数

当调用函数时,this参数也会默认地传给参数。this参数代表了函数调用相关联的对象,也就是函数上下文。

4.2 函数调用

我们可以通过四个方法调用一个函数,每种方法都有一些细微的差距

  • 作为一个函数,直接被调用。
  • 作为一个方法,关联在一个对象上实现面向对象的编程
  • 作为一个构造函数实例化一个对象
  • 通过apply和call进行回调

4.2.1 作为函数被调用

当我们通过()运算符调用一个函数,且被执行的函数不作为一个属性存在在某个对象上,这种就是作为函数被调用,函数上下文(this)有两种可能,在严格模式下他是undefined,在非严格模式下,他将是全局对象(如果是在浏览器环境运行,则是window对象)。

4.2.2 作为方法被调用

当函数作为某个对象的方法被调用时,该对象会成为函数的上下文。并且在函数内部可以通过参数访问到。

// 函数调用和方法调用的区别
function whatsMyContext() {
  // 返回函数上下文,从而让我们能从函数外面检查函数上下文
  return this;
}

// 作为函数被调用并将其上下文设置为window对象
console.log(
  "作为函数被调用时,起对象是否为window对象",
  whatsMyContext() === window
);

// 变量getMyThis得到了函数的whatsMyContext的引用
const getMyThis = whatsMyContext;

// 使用变量getMyThis来屌用函数,该函数仍然作为函数被调用,函数上下文也依然是window对象
console.log("getMythis的上下文是?", getMyThis());

// 创建一个对象ninja1,其属性getMyThis得到了函数whatsMyContext的引用
const ninja1 = {
  getMyThis: whatsMyContext,
};

// 使用ninja1对象的方法getMyThis来调用函数。函数上下文现在是ninja1了,这就是面向对象
console.log("该函数的上下文是?", ninja1.getMyThis());

// 用相同的方式创建一个ninja2对象
const ninja2 = {
  getMyThis: whatsMyContext,
};

// 不难发现现在函数的上下文编程了ninja2
console.log("该函数的上下文是?", ninja2.getMyThis());
// 函数调用和方法调用的区别
function whatsMyContext() {
  // 返回函数上下文,从而让我们能从函数外面检查函数上下文
  return this;
}

// 作为函数被调用并将其上下文设置为window对象
console.log(
  "作为函数被调用时,起对象是否为window对象",
  whatsMyContext() === window
);

// 变量getMyThis得到了函数的whatsMyContext的引用
const getMyThis = whatsMyContext;

// 使用变量getMyThis来屌用函数,该函数仍然作为函数被调用,函数上下文也依然是window对象
console.log("getMythis的上下文是?", getMyThis());

// 创建一个对象ninja1,其属性getMyThis得到了函数whatsMyContext的引用
const ninja1 = {
  getMyThis: whatsMyContext,
};

// 使用ninja1对象的方法getMyThis来调用函数。函数上下文现在是ninja1了,这就是面向对象
console.log("该函数的上下文是?", ninja1.getMyThis());

// 用相同的方式创建一个ninja2对象
const ninja2 = {
  getMyThis: whatsMyContext,
};

// 不难发现现在函数的上下文变成了ninja2
console.log("该函数的上下文是?", ninja2.getMyThis());

根据上面的代码我们不难发现虽然ninja1对象和ninja2对象都引用了相同的函数,但是函数返回的上下文却并不相同,而是取决于调用他们的对象,这也就是说在我们这个函数是我们可以调用这个函数上的对象,因此我们不需要创建一个单独的函数副本来操作不同的对象执行相同的操作,这也就是面向对象的美丽所在。

4.2.3作为构造函数调用

在他作为构造函数调用前,我们要使用关键字new。 我们可以使用构造函数来实现对象:

/**
 * 构造函数创建一个独享,并在该对象也就是函数上下文添加一个属性skulk.这个
 * skulk方法再次返回函数上下文,从未能热昂我们函数外部检测函数上下文
 *  */
function Ninja() {
  this.skuilk = function () {
    return this;
  };
}

// 通过关键字new调用构造函数从而创建两个新对象。变量ninja1和变量ninja2分别引用了这两个对象
const ninja1 = new Ninja();
const ninja2 = new Ninja();

// 检测已创建对象中的skulk方法。每个方法都应该返回自身已创建的对象
console.log(
  "执行ninja1对象中的方法查看其上下文的值是本对象",
  ninja1.skuilk() === ninja1
);
console.log(
  "执行ninja2对象中的方法查看其上下文的值是本对象",
  ninja2.skuilk() === ninja1
);

一般来讲,作为构造函数被调用时,会发生以下的步骤:

  1. 创建一个新的空对象
  2. 该对象作为this参数传递给构造函数,从而成为构造函数的函数上下文。
  3. 新构造的对象作为new运算符的返回值
构造函数返回值
// 定义一个Ninja的构造函数
function Ninja() {
  this.skulk = function () {
    return true;
  };

  // 构造函数返回一个原始类型值,即数字1
  return 1;
}

// 该函数以函数的形式被调用,正如预期,其返回值为数字1
console.log(
  "当没有作为构造函数被使用时,会返回我们定义好的返回值吗",
  Ninja() === 1
);

// 该函数现在正在以构造函数的形式被调用
const ninja = new Ninja();

// 测试标明返回值1被忽略了,一个新的被初始化的对象被通过关键字new所返回
console.log(
  "当函数作为构造函数被调用时,会忽略我门定义好的返回值从而返回一个新的返回值吗",
  typeof ninja === "object"
);
console.log("ninja对象是否会有skulk函数呢", typeof ninja.skulk === "function");
现在我们可以尝试对构造函数做一些修改,让他去返回一个另一个对象
// 创建一个对象,该对象rules属性设置为FALSE
const puppet = {
  rules: false,
};

// 尽管初始化了传入的this对向,返回该全局对象
function Emperor() {
  this.rules = true;
  return puppet;
}

// 作为构造函数使用该对象
const emperor = new Emperor();

// 测试表明,变量emperor的值为由构造函数返回的对象,而不是new表达式所返回的对象
console.log("这个皇帝难道是一只小狗", emperor === puppet);

现在我们可以对这些现象做一些总结:

  • 如果构造函数返回一个对象,则该对象作为整个表达式的值返回,而构造函数中的this将被抛弃。
  • 但是,如果返回的是非对象类型,则忽略返回值,返回新创建的对象。

4.2.4 使用apply和call方法调用

通过以上的例子我们可以发现不同类型函数调用之间的差距在于:最终作为函数上下文(可以通过this参数隐式引用到)传递给执行函数的对象不同。、

接下来让我们来看一个错误的示例。

/**
 * 为对象赋值事件处理器的构造函数,该事件处理器反映了按钮的状态。通过这个事件处理器
 * 反映了按钮的状态。通过这事件处理器,我们能够跟踪按钮是否被单击
 *  */
function Button() {
  this.clicked = false;

  /**
   * 单击事件处理器的声明函数。由于该函数是对象的方法,所以在函数中使用this来获取
   * 对对象的引用
   *  */ 
  this.click = function () {
    this.clicked = true;

    /**
     * 在该方法中,我们检测按钮是否中的,正确的改变了状态,发现并没有,因为这里的上下
     * 文是button标签
     *  */  
    console.log("这个按钮是否已经被点击", button.clicked);
  };
}

// 创建一个用于跟踪按钮是否被单击的实例
const button = new Button();

// 为按钮上添加单击处理器
const elem = document.getElementById("test");

// 在按钮上添加单击事件处理器
elem.addEventListener("click", button.click);

通过这个示例我们发现函数的上下文往往是调用他的对象,而有时我们则想让他指向我们所期望的对象。这时,我们就需要call和apply函数的帮忙,他们可以显示的设置函数上下文。我们可以使用每个函数上都存在的这两个方法来完成。(call和apply的存在也证明了函数是第一类对象的存在)

// 函数处理了参数,并将结果放在任意一个作为该函数上下文的对象上
function juggle() {
  let result = 0;
  for (var n = 0; n < arguments.length; n++) {
    result += arguments[n];
  }
  this.result = result;
}

// 这些对象的初始值为空,它们会作为测试对象
const ninja1 = {};
const ninja2 = {};

// 使用apply方法向ninja1传递一个参数数组
juggle.apply(ninja1, [1, 2, 3, 4]);

// 使用call方法向ninja2传递一个参数列表
juggle.call(ninja2, 5, 6, 7, 8);

// 测试输出结果,发现一切正确
console.log("juggled via apply", ninja1.result === 10);
console.log("juggled via call", ninja2.result === 26);

image.png

传入call和apply方法的第一个参数都会被作为函数上下文,而后面的参数则是需要传递给函数的参数

image.png

4.3 解决函数上下文问题

虽然说call和apply这两个函数上的方法都可以很有效的改变函数的上下文,但是这一些不同的情况下,箭头函数和bind可以更优雅的解决此类问题。

4.3.1 使用箭头函数绕过函数上下文

箭头函数有一个特别的点,就是他没有单独的this值。箭头函数的this与声明所在的上下文的相同。

接下来我们可以使用箭头函数解决之前的问题。

// Button构造函数用于创建保存按钮的状态的对象
function Button() {
  this.clicked = false;

  // 声明用于处理点击事件的箭头函数。因为是click对象的方法。我们在函数内部使用this获得对象的引用
  this.click = () => {
    this.clicked = true;
    // 发现现在的clicked值已经正常被修改了
    console.log("这个按钮是否已经被点击", button.clicked);
  };
}
const button = new Button();
const elem = document.getElementById("test");
elem.addEventListener("click", button.click);

调用箭头函数时,不会隐式的传入this参数,而是从定义时的函数继承(这是个需要注意的点,也就是说你在字面量对象里创建箭头函数他也不会指向这个字面量对象而是包裹他的函数)上下文。在本例中,箭头函数指向了构造函数内部,在实例化的时候this指向了新创建的实例对象上。

4.3.2 使用bind方法

函数除了call,apply还可以访问bind,并通过他创建新的函数(这个函数的函数体与旧函数的函数体相同,但是上this指向却变成了新指定的对象上)。 接下来我们通过一段代码来实现对bind的学习

const button = {
  clicked: false,
  click: function () {
    this.clicked = true;
    console.log("这个按钮是否被点击", button.clicked);
  },
};
const elem = document.getElementById("test");
elem.addEventListener("click", button.click.bind(button));
const boundFunction = button.click.bind(button);
console.log("调用bind时是否创建了一个新的函数", boundFunction != button.click);

记住bind不会修改原函数而是创建一个新函数

chapter5 精通函数:闭包和作用域

5.1 理解闭包

何为闭包?一个引用了其他函数作用域中的变量的函数。

//一个简单的闭包
//在全局作用域中定义一个变量
const otherValue = "ninja";

//全局作用域中声明函数
const otherFunction() {
  console.log( otherValue === 'ninja' );
}

//执行该函数
otherFunction()

在这行代码中我们的otherFunction可以很轻松的获得外部变量。其实这就是一个闭包。

const outerValue = "samurai";
let later;
function outerFunction() {
  var innerValue = "ninja";
  function innerFunction() {
    console.log("我可以看见ninja吗", innerValue === "ninja");
    console.log("我可以看见samurai吗", outerValue === "samurai");
  }

  later = innerFunction;
}
outerFunction();
//这个函数并不会执行,因为他的作用域被限制在外部函数outerFunction之内。
later();

通过执行上面的代码我们可以看到内部函数的作用域消失后之后再执行内部函数时,其变量依然存在。 当外部函数定义内部函数时,不仅定义了函数的声明,而且还创建了一个闭包。该闭包不仅包含了函数的声明还包含了 函数声明时该作用域中的所有变量。当最终执行内部函数时,尽管声明时的作用域已经消失。但是通过闭包仍然能访问到原始作用域。

image.pngsdsdsdsdsdsdsd

正如上图的保护气泡一样,只要内部函数一直存在,内部函数就一直保存着该函数的作用域中的变量

这就是闭包。闭包创建了被定义时的作用域内的变量和函数的安全气泡,因此获得了执行时所需的内容。 每一个通过闭包访问变量的函数都具有一个作用域链(其包含了闭包的所有信息),存储和引用这些信息会直接影响性能,直到JavaScript确认这些信息不会再被调用时或者页面卸载时才会被销毁。

5.2 使用闭包

5.2.1 封装私有变量

// 定义ninjia构造函数
function Ninja() {
  /**
   *  在构造函数内部声明一个变量,因为所声明的变量的作用域局限于构造函数的内部,所以我们
   * 可以将他看做是一个私有变量,我们使用这个变量来统计ninja佯攻的次数
   */
  var feints = 0;

  // 创建用于访问计数变量feints的方法。
  this.getFeints = function () {
    return feints;
  };

  // 我们可以使用这个方法来添加佯装的个数
  this.feint = function () {
    feints++;
  };
}

const ninja1 = new Ninja();
ninja1.feint();

// 输出是undefined,这表示我们不能直接获取
console.log(ninja1.feints);

// 我们尝试通过闭包去访问,显示1表示构造函数中的feint确实可以操作函数外部的变量,getFeints方法也能正确访问到佯装值
console.log(ninja1.getFeints());
const ninja2 = new Ninja();
// 输出的结果为0表示当我们通过Ninja构造函数创建的每个新实例都具有自己的私有变量
console.log(ninja2.getFeints())
//其实我并不是很理解为什么ninja2的feint值为0。正常来说只要这个函数被调用他就不应该被销毁啊。

5.2.2回调函数

回调函数是另一种使用闭包的常见场景

function animate(elemId) {
        // 在动画函数animate内部,获取DOM元素引用
        const elem = document.getElementById(elemId);
        let tick = 0;
        // 创建一个计时器,并传入一个回调函数,用于记录动画的执行次数
        let 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 referce also obtianed via a closure");
          }
        }, 0);
      }
      
      // 执行这个函数
      animate("box1");

在这段代码里动画通过匿名函数进行了实现,这个匿名函数通过3个变量控制动画的过程:elem,tick和timer。同样我们也可以把这三个变量放在全局作用域里,但是这样就会导致不同的状态却使用相同的数值从而让动画变得的更加冲突。 而闭包的概念就很好的解决了上面动画冲突的问题,正是通过闭包使得在计时器中的回调函数可以访问这些变量。每个动画都能够获得属于自己的私有变量。

image.png

5.3 通过执行上下文来跟踪代码

之前介绍了JavaScript中有两种类型的代码,一种是全局代码一种是函数代码,两种代码就有两种上下文:全局上下文和函数执行上下文,全局上下文从JavaScript程序开始执行时就已经创建;而函数上下文则是从函数被调用的时候开始创建。

也介绍了JavaScript的单线程模式:在某个特定的时刻只能执行特定的代码。一旦发生函数调用当前函数上下文必须停止执行,并创建新的函数执行上下文来执行函数,新的函数执行完毕之后将其函数上下文销毁。并重新回到发生调用时的执行上下文中。

image.png

5.4 使用词法环境变量跟踪变量的作用域

词法环境(lexical environment)是JavaScript引擎内部用来跟踪标识符与特定变量之间的映射关系。

const ninja = "Hattori";
console.log(ninja)

当在console.log访问ninja变量时,就会进行词法环境的查询。 词法环境与特定的JavaScript代码结构关联。既可以是一段函数,是一个块,也可以是一个try-catch语句。

5.4.1 代码嵌套

词法环境主要用于代码嵌套

image.png 代码可以访问他的外部变量。例如for循环可以访问report函数,skulk函数以及全局代码中的变量,report函数则可以访问skulk函数中的变量。而词法环境(作用域)帮我们分析哪些代码是我们可以访问的。

image.png 简单来说就是从内往外找资源。

5.6 研究闭包的工作原理

5.6.1 回顾使用闭包模拟私有变量的代码

// 定义ninjia构造函数
function Ninja() {
  /**
   *  在构造函数内部声明一个变量,因为所声明的变量的作用域局限于构造函数的内部,所以我们
   * 可以将他看做是一个私有变量,我们使用这个变量来统计ninja佯攻的次数
   */
  var feints = 0;

  // 创建用于访问计数变量feints的方法。
  this.getFeints = function () {
    return feints;
  };

  // 我们可以使用这个方法来添加佯装的个数
  this.feint = function () {
    feints++;
  };
}

const ninja1 = new Ninja();
ninja1.feint();

// 输出是undefined,这表示我们不能直接获取
console.log(ninja1.feints);

// 我们尝试通过闭包去访问,显示1表示构造函数中的feint确实可以操作函数外部的变量,getFeints方法也能正确访问到佯装值
console.log(ninja1.getFeints());
const ninja2 = new Ninja();
// 输出的结果为0表示当我们通过Ninja构造函数创建的每个新实例都具有自己的私有变量
console.log(ninja2.getFeints())

image.png 我们通过new调用构造函数,每次调用的时候都会生成一个新的词法环境。该词法环境保持函数内部的局部变量。

image.png 但是这并不是真正的私有变量,他实例化的对象的方法如果赋值给其他对象,其他对象仍然可以访问到。

chapter6 未来的函数:生成器和promise

6.1 使用生成器函数

// 通过在function后面添加一个*号创建一个生成器函数
function* weaponGenertor() {
  // 使用新的关键字yield生成独立的值
  yield "gun";
  yield "knife";
  yield "sword";
}

// 使用新的循环类型类型for-of取出生成的值序列
for (let weapon of weaponGenertor()) {
  console.log(weapon);
  /**
   * output:
   * gun
   * knife
   * sword
   */
}

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

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

// 定义一个生成器,他能生成一个包含两个武器数据的序列
function* weaponGenertor() {
  yield "sword";
  yield "gun";
}

//调用生成器得到一个迭代器,从而我们能够控制生成器的执行
const weaponsIterator = weaponGenertor();

// 调用迭代器的next方法向生成器请求一个新值
const result1 = weaponsIterator.next();

// output:{ value: 'sword', done: false }
console.log(result1);

const result2 = weaponsIterator.next();

// output:{ value: 'gun', done: false }
console.log(result2);

const result3 = weaponsIterator.next();

// output:{ value: undefined, done: true }
console.log(result3);
/**
 * 当没有可执行的代码生成器就返回一个对象({ value: undefined, done: true })
 * 表示当前没有可执行的值,且已经过完了一次迭代
 */

next函数调用后生成器就开始执行代码,当代码执行到yield,就会生成一个中间结果(生成值序列中的一项),然后返回一个新对象。其中封装了结果值和一个指示完成的指示器。每生成一个当前值后,生成器就会非阻塞的挂起

对迭代器进行迭代
function* weaponGenertor() {
  yield "knife";
  yield "sword";
}

// 新建一个迭代器
const weaponsIterator = weaponGenertor();

// 创建一个变量,用他来保存这个生成器产生的值
let item;

// 每次循环都会取出一个值。当生成器不会再生成值得时候,停止迭代
while (!(item = weaponsIterator.next()).done) {
  console.log(item.value);
}

看到上面的代码了吗,这就是for...of的循环原理,for...of不过是对迭代器进行迭代的语法糖

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

正如函数调用另一个函数一般,我们也可以尝试将生成器的执行委托给另一个生成器

// 这是一个战士生成器
function* WarriorGenerator() {
  yield "sun Tzu";

  // 在这里的时候yield* 将执行权交给了另一个生成器
  yield* NinjaGenerator();
  yield "Genghis Khan";
}

// 这是一个忍者生成器
function* NinjaGenerator() {
  yield "Mike";
  yield "Smith";
}

for (let warrior of WarriorGenerator()) {
  console.log(warrior);
}
/**
* 最后输出的结果:
* sun Tzu
* Mike
* Smith
* Genghis Khan
/

那为什么会直接输出Mike和Smith呢?

因为当迭代器上使用yield* 操作符,程序会跳到另一个生成器上执行。在本例中,程序从WarriorGenerator跳转到NinjaGenerator生成器上。每次调用warriorGenerator返回迭代器的next方法都会使执行重新寻找到NinjaGenerator上直到执行权无工作可做。注意,对于调用最初的迭代器代码来说,这一切都是透明的,for。。。of不会关心其中的过程,他只关心在done状态来之前一直调用next方法。

6.1.2 使用生成器

用生成器生成ID序列:当我们创建某个对象时,经常需要为某个对象赋唯一的ID值。

// 定义一个生成器函数IdGenerator
function* IdGenerator() {

  // 一个始终记录ID的变量,这个变量无法在生成器外部改变。
  let id = 0;
  
  // 循环成无限长度的ID序列
  while (true) {
    yield ++id;
  }
}

// 通过调用生成器去创建一个迭代器,我们通过它来控制生成器的执行。
const IdIterator = IdGenerator();

const ninja1 = { id: IdIterator.next().value };
const ninja2 = { id: IdIterator.next().value };
const ninja3 = { id: IdIterator.next().value };

console.log(ninja1, ninja2, ninja3);
// output:{ id: 1 } { id: 2 } { id: 3 }

局部变量id仅能在该生成器中被访问,完全性大大提高。正常来说我们不应该写一个死循环,但是他包含了yield就不一样了,他不会一次跑完循环,每次的执行都会等待上一次执行的结束。

6.1.3与生成器交互

生成器的参数使用

// 生成器可以向其他函数一样接受标准参数
function* NinjaGenerator(action) {
  const imposter = yield ("Hattori " + action);
  // 其实我很好奇为什么imposter会等于Hanzo呢
  console.log(imposter === "Hanzo",
  "The generator has been infiltrated");
  yield ("Yoshi (" + imposter + ") " + action);
  }

  // 接下来就是普通的参数传递
  const ninjaIterator = NinjaGenerator("skulk");
  
  const result1 = ninjaIterator.next();
  console.log(result1.value === "Hattori skulk","Hattori is skulking");

  const result2 = ninjaIterator.next("Hanzo");
  console.log(result2.value === "Yoshi (Hanzo) skulk",
  "We have an imposter!");

image.png 接下来我们可结合图片进行一些分析

  1. 声明了一个带有参数的生成器
  2. 执行生成器并传入一个“skulk”的字符串
  3. 执行下一行语句,我们调用了迭代器的next方法,于是开始执行迭代器中的代码,然而我们可以看到这是一个表达式,而且是等号连接所以先执行右边然后我们就会执行yield ("Hattori " + action),然后将计算的结果直接返回并用result1进行保存
  4. 再次执行下一行代码,同样是调用next,并传入“Hanzo”字符串,并将传入的“hanzo”作为yield表达式的值传给imposter。

抛出异常

// 向生成器抛出异常
function* NinjaGenerator() {
  try {
    yield "Hattori";

    // 此处的错误将不会发生
    console.log("期待的表达式并没有发生");

    // 捕获异常并检测接收到的异常啥都符合预期
  } catch (e) {
    console.log("啊哈!我们获取到了一个表达式", e === "hi");
  }
}

const ninjaIterator = NinjaGenerator();
const result1 = ninjaIterator.next();

// output: Hattori
console.log(result1.value);

/**
 * throw在仁和迭代器都有用
 * output: 啊哈!我们获取到了一个表达式 true
 */
ninjaIterator.throw("hi");

6.2.3 探索生成器内部构成

在某种方面来说,生成器的工作更像是一个小程序,一个在状态中运行的状态机。

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

image.png

通过执行上下文跟踪生成器函数

之前我们也回顾了函数上下文,他是一个用于跟踪函数的执行的JavaScript内部机制。

function* NinjaGenerator(action) {
  yield "Hattori " + action;
  return "Yoshi" + action;
}

const ninjaIterator = NinjaGenerator("skulk");
const result1 = ninjaIterator.next();
const result2 = ninjaIterator.next();

image.png 这张图片有两个快照。第一个快照显示的是应用在调用生成器之前的状态由于当前执行的是全局代码,故而上下文栈只包含全局上下文,第二个显示的是执行生成器时的状态,一般情况下,当一个程序从一个标准函数返回后,对应的执行上下文会从栈中弹出,并被完整的销毁但是生成器不一样。

image.png 虽然他会从上下文栈中被弹出,但是仍然可以通过迭代器被访问。就像闭包一样,我们依然通过内部的函数访问到往外部的变量。

当然调用next方法的表现也很不同,他会重新激活对应上下文。

image.png 函数每次被调用的时候都会创建一个全新的上下文,而生成器的上下文则并不会改变。他只会暂时挂起,等待下次迭代器的调用。

6.3 使用promise

第三部分 专研对象,强化代码

chapter7 面向对象和原型

7.1 理解原型

在JavaScript中,对象是属性名和属性值的集合。例如,我们可以简单地创建一个对象字面量。

const abc = {name: zx}

在软件开发中,为了避免重复造轮子,我们希望尽可能的复用代码。而继承则是代码复用的一种方式,继承有助于合理的组织程序代码。将一个对象的属性扩展到另一个对象上。而在JavaScript中,可通过原型实现继承。

每个对象上都有其原型的引用,当我们在对象上找不到属性时,就会在其原型上寻找。

// 对象可以通过原型访问其他对象的属性

// 创建3个带有属性的对象
const dog = { wangwangwang: true };
const cat = { miaomiaomiao: true };
const snake = { sisisi: true };

//确认每个对象中都有一个相对应的属性存在
console.log("dog can say wangwangwang", "wangwangwang" in dog);
console.log("cat can say miaomiaomiao", "miaomiaomiao" in cat);
console.log("snake can say sisisi", "sisisi" in snake);
/**
 * output:
 * dog can say wangwangwang true
 * cat can say miaomiaomiao true
 * snake can say sisisi true
 */

// 将cat对象设置为dog的原型
Object.setPrototypeOf(dog, cat);

/**
 * output:dog can say wangwangwang and miaomiaomiao true
 * 通过输出的结果我们可以发现将cat设置为dog的原型之后,dog也可以访问cat属性了
 *  */ 
console.log(
  "dog can say wangwangwang and miaomiaomiao",
  "wangwangwang" in dog && "miaomiaomiao" in dog
);

每个对象都可以有一个原型,而原型本身也是对象,也可有属于他的原型,以此类推,形成了一个原型链,寻找属性就是按照原型链从底至顶的顺序开始寻找。

7.2 对象构造器与原型

我们可以通过对象字面量来创建一个最简单的对象。但是有时候我们想要创建多个相同类型的对象的实例,如果还是用字面量的方式去创建,不仅繁琐,还容易出错。我们希望能够将这些对象的方法和属性整合成一个类。

接下来我们可以尝试通过原型方法来创建一个新的实例

// 定义一个空函数,什么也不做,也没有返回值
function Ninja() {}

// 每个函数都具有可置的原对象,我们可以对其进行自由更改
Ninja.prototype.swingSword = function () {
  return true;
};

// 将函数作为一个普通函数进行调用。确认他没有任何返回值
const ninja1 = Ninja();
console.log("Ninja并没有创建实例", ninja1 === undefined);

// 将函数作为一个构造函数进行调用,确认其不经创造了新的实例,并且该实例上具有原型上的方法
const ninja2 = new Ninja();
console.log(
  "实例已经被创造且其上面的方法可以被调用",
  ninja2 && ninja2.swingSword && ninja2.swingSword()
);

在这段代码中我们创建了一个Ninja函数并分别以普通函数和构造函数的形式调用了他。当我们以普通的函数调用他的时候函数没有返回值,因为没有返回值所以ninja1也就不会有原型啦,但当我们以构造函数调用他的时候一切都发生了改变,首先他创建了一个实例,并在这个实例上

第四部分 洞悉浏览器