JavaScript笔记1

1.ES6 声明变量的六种方法 :

 let ,var,const,import,class,function;

2.三种比较操作符

JavaScript 提供了三种比较操作符,包括严格比较操作符===和非严格的比较操作符 ==,以及 Object.is() 方法。

Object.is() 确定两个值是否为相同值。如果以下其中一项成立,则两个值相同:

  • 都是 undefined
  • 都是 null
  • 都是 true 或者都是 false
  • 都是长度相同、字符相同、顺序相同的字符串
  • 都是相同的对象(意味着两个值都引用了内存中的同一对象)
  • 都是 BigInt 且具有相同的数值
  • 都是 symbol 且引用相同的 symbol 值
  • 都是数字且
    • 都是 +0
    • 都是 -0
    • 都是 NaN
    • 都有相同的值,非零且都不是 NaN

Object.is() 与 == 运算符并不等价。== 运算符在测试相等性之前,会对两个操作数进行类型转换(如果它们不是相同的类型),这可能会导致一些非预期的行为,例如 "" == false 的结果是 true,但是 Object.is() 不会对其操作数进行类型转换。

Object.is() 也等价于 === 运算符。Object.is() 和 === 之间的唯一区别在于它们处理带符号的 0 和 NaN 值的时候。=== 运算符(和 == 运算符)将数值 -0 和 +0 视为相等,但是会将 NaN 视为彼此不相等。

3.逻辑与(&&)

逻辑与(&&)运算符从左到右对操作数求值,遇到第一个假值操作数时立即返回;如果所有的操作数都是真值,则返回最后一个操作数的值。

能够转化为 true 的值叫做真值,能够转化为 false 的值叫做假值

能够转化为 false 的表达式的示例如下:

  • false
  • null
  • NaN
  • 0
  • 空字符串("" 或 '' 或 ``);
  • undefined

    与运算符会保留所有非布尔值,并原样返回它们:

    result = "" && "foo"; // 结果被赋值为 ""(空字符串)
    result = 2 && 0; // 结果被赋值为 0
    result = "foo" && 4; // 结果被赋值为 4
    

    尽管 && 运算符可以与非布尔操作数一起使用,但它仍然被认为是一个布尔运算符,因为它的返回值总是可以被转换为布尔基本类型。要明确地将其返回值(或任何一般的表达式)转换为相应的布尔值,请使用双非运算符或 Boolean 构造函数。

  • 可以使用几个非运算符串联起来,明确地强制将任何值转换为相应的布尔基本类型。这种转换是基于值的“真实性”或“虚假性”(详见真值假值)  !    !!

4.逻辑或(||)

对于一组操作数的逻辑或||,逻辑析取)运算符,当且仅当其一个或多个操作数为真,其运算结果为真。它通常与布尔(逻辑)值一起使用。当它是布尔值时,返回一个布尔值。然而,|| 运算符实际上是返回一个指定的操作数的值,所以如果这个运算符被用于非布尔值,它将返回一个非布尔值

5.内存管理

标记 - 清除算法

这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。

这个算法假定设置一个叫做根(root)的对象(在 Javascript 里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。

这个算法比前一个要好,因为“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”。

从 2012 年起,所有现代浏览器都使用了标记 - 清除垃圾回收算法。所有对 JavaScript 垃圾回收算法的改进都是基于标记 - 清除算法的改进,并没有改进标记 - 清除算法本身和它对“对象是否不再需要”的简化定义。

 6.标准内置对象分类

值属性

这些全局属性返回一个简单值,这些值没有自己的属性和方法。

函数属性

全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。

基本对象

基本对象是定义或使用其他对象的基础。

错误对象

错误对象是一种特殊的基本对象。它们拥有基本的 Error 类型,同时也有多种具体的错误类型。

数字和日期对象

用来表示数字、日期和执行数学计算的对象。

字符串

这些对象表示字符串并支持操作字符串。

可索引的集合对象

这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。

使用键的集合对象

这些集合对象在存储数据时会使用到键,包括可迭代的 Map 和 Set,支持按照插入顺序来迭代元素。

结构化数据

这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON(JavaScript Object Notation)编码的数据。

内存管理对象

这些对象会与垃圾回收机制产生交互。

控制抽象对象

控件抽象对象可以帮助构造代码,尤其是异步代码(例如不使用深度嵌套的回调)。

反射

国际化

ECMAScript 核心的附加功能,用于支持多语言处理。

7.类

类是用于创建对象的模板。他们用代码封装数据以处理该数据。实际上,类是“特殊的函数”,就像你能够定义的函数表达式函数声明一样,类语法有两个组成部分:类表达式类声明

类声明

定义类的一种方法是使用类声明。要声明一个类,你可以使用带有class关键字的类名(这里是“Rectangle”)。

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

提升

函数声明类声明之间的一个重要区别在于,函数声明会提升,类声明不会。你首先需要声明你的类,然后再访问它,否则类似以下的代码将抛出ReferenceError

let p = new Rectangle(); // ReferenceError

class Rectangle {}

类表达式

类表达式是定义类的另一种方法。类表达式可以命名或不命名。命名类表达式的名称是该类体的局部名称。(不过,可以通过类的 (而不是一个实例的) name 属性来检索它)。

构造函数

constructor方法是一个特殊的方法,这种方法用于创建和初始化一个由class创建的对象。一个类只能拥有一个名为“constructor”的特殊方法。如果类包含多个constructor的方法,则将抛出 一个SyntaxError 。

静态方法

static 关键字用来定义一个类的一个静态方法。调用静态方法不需要实例化 (en-US) 该类,但不能通过一个类实例调用静态方法。静态方法通常用于为一个应用程序创建工具函数。

实例属性

实例的属性必须定义在类的方法里:

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

静态的或原型的数据属性必须定义在类定义的外面。

Rectangle.staticWidth = 20;
Rectangle.prototype.prototypeWidth = 25;

字段声明

警告: 公共和私有字段声明是 JavaScript 标准委员会TC39提出的实验性功能(第 3 阶段)。浏览器中的支持是有限的,但是可以通过Babel等系统构建后使用此功能。

公有字段声明

使用 JavaScript 字段声明语法,上面的示例可以写成:

class Rectangle {
  height = 0;
  width;
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

通过预先声明字段,类定义变得更加自我记录,并且字段始终存在。

在声明字段时,我们不需要像 letconst 和 var 这样的关键字。

正如上面看到的,这个字段可以用也可以不用默认值来声明。

私有字段声明

使用私有字段,可以按以下方式细化定义。

class Rectangle {
  #height = 0;
  #width;
  constructor(height, width) {
    this.#height = height;
    this.#width = width;
  }
}

Copy to ClipboardCopy to Clipboard

从类外部引用私有字段是错误的。它们只能在类里面中读取或写入。通过定义在类外部不可见的内容,可以确保类的用户不会依赖于内部,因为内部可能在不同版本之间发生变化。

备注: 私有字段仅能在字段声明中预先定义。

私有字段不能通过在之后赋值来创建它们,这种方式只适用普通属性。

extends 关键字在 类声明 或 类表达式 中用于创建一个类作为另一个类的一个子类。

8.数据类型

最新的 ECMAScript 标准定义了 8 种数据类型:

  • 七种基本数据类型:
    • 布尔值(Boolean),有 2 个值分别是:true 和 false.
    • null,一个表明 null 值的特殊关键字。JavaScript 是大小写敏感的,因此 null 与 NullNULL或变体完全不同。
    • undefined,和 null 一样是一个特殊的关键字,undefined 表示变量未赋值时的属性。
    • 数字(Number),整数或浮点数,例如: 42 或者 3.14159
    • 任意精度的整数 (BigInt) ,可以安全地存储和操作大整数,甚至可以超过数字的安全整数限制。
    • 字符串(String),字符串是一串表示文本值的字符序列,例如:"Howdy" 。
    • 代表(Symbol)( 在 ECMAScript 6 中新添加的类型).。一种实例是唯一且不可改变的数据类型。
  • 以及对象(Object)。

9.对象字面量 (Object literals)

对象属性名字可以是任意字符串,包括空串。如果对象属性名字不是合法的 javascript 标识符,它必须用""包裹。属性的名字不合法,那么便不能用。访问属性值,而是通过类数组标记 ("[]") 访问和赋值。

var unusualPropertyNames = {
  "": "An empty string",
  "!": "Bang!"
}
console.log(unusualPropertyNames."");   // 语法错误:Unexpected string
console.log(unusualPropertyNames[""]);  // An empty string
console.log(unusualPropertyNames.!);    // 语法错误:Unexpected token !
console.log(unusualPropertyNames["!"]); // Bang!

属性的简洁表示法 § 

ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。

let birth = '2000/01/01';

const Person = {

  name: '张三',

  //等同于birth: birth
  birth,

  // 等同于hello: function ()...
  hello() { console.log('我的名字是', this.name); }

};

10.模板字符串

模板字面量是用反引号(`)分隔的字面量,允许多行字符串、带嵌入表达式的字符串插值和一种叫带标签的模板的特殊结构。

模板字面量用反引号(`)括起来,而不是双引号(")或单引号(')。 除了普通字符串外,模板字面量还可以包含占位符——一种由美元符号和大括号分隔的嵌入式表达式:${expression}。字符串和占位符被传递给一个函数(要么是默认函数,要么是自定义函数)。默认函数(当未提供自定义函数时)只执行字符串插值来替换占位符,然后将这些部分拼接到一个字符串中。

11

异常处理语句

你可以用 throw 语句抛出一个异常并且用 try...catch 语句捕获处理它。

异常类型

JavaScript 可以抛出任意对象。然而,不是所有对象能产生相同的结果。尽管抛出数值或者字母串作为错误信息十分常见,但是通常用下列其中一种异常类型来创建目标更为高效:

12.Error

当运行时错误产生时,Error 对象会被抛出。Error 对象也可用于用户自定义的异常的基础对象。下面列出了各种内建的标准错误类型。

描述

当代码运行时的发生错误,会创建新的 Error 对象,并将其抛出。

错误类型

除了通用的 Error 构造函数外,JavaScript 还有其他类型的错误构造函数。对于客户端异常,详见异常处理语句

EvalError

创建一个 error 实例,表示错误的原因:与 eval() 有关。

RangeError

创建一个 error 实例,表示错误的原因:数值变量或参数超出其有效范围。

ReferenceError

创建一个 error 实例,表示错误的原因:无效引用。

SyntaxError

创建一个 error 实例,表示错误的原因:语法错误。

TypeError

创建一个 error 实例,表示错误的原因:变量或参数不属于有效类型。

URIError

创建一个 error 实例,表示错误的原因:给 encodeURI() 或 decodeURI() 传递的参数无效。

AggregateError

创建一个 error 实例,其中包裹了由一个操作产生且需要报告的多个错误。如:Promise.any() 产生的错误。

InternalError 非标准

创建一个代表 Javascript 引擎内部错误的异常抛出的实例。如:递归太多。

13.循环与迭代

JavaScript 中提供了这些循环语句:

continue 语句的语法看起来像这样:

continue [label];

break 语句

使用 break 语句来终止循环,switch,或者是链接到 label 语句。

当你使用不带 label 的 break 时,它会立即终止当前所在的 whiledo-whilefor,或者 switch 并把控制权交回这些结构后面的语句。 当你使用带 label 的 break 时,它会终止指定的带标记(label)的语句。

break 语句的语法看起来像这样:

break [label];

label 语句

一个 label 提供了一个让你在程序中其他位置引用它的标识符。例如,你可以用 label 标识一个循环,然后使用 break 或者 continue 来指出程序是否该停止循环还是继续循环。

for...in 语句(循环属性)

for...in 语句循环一个指定的变量来循环一个对象所有可枚举的属性。JavaScript 会为每一个不同的属性执行指定的语句。

for...of 语句(循环值)

for...of 语句在可迭代对象(包括ArrayMapSetarguments 等等)上创建了一个循环,对值的每一个独特属性调用一次迭代。

下面的这个例子展示了 for...of 和 for...in 两种循环语句之间的区别。 for...in 循环遍历的结果是数组元素的下标,而 for...of 遍历的结果是元素的值

14.

定义函数(理解:函数声明与函数表达式的区别)

函数声明

一个函数定义(也称为函数声明,或函数语句)由一系列的function关键字组成,依次为:

  • 函数的名称。
  • 函数参数列表,包围在括号中并由逗号分隔。
  • 定义函数的 JavaScript 语句,用大括号{}括起来。

函数表达式

虽然上面的函数声明在语法上是一个语句,但函数也可以由函数表达式创建。这样的函数可以是匿名的;它不必有一个名称。例如,函数square也可这样来定义:

const square = function(number) { return number * number; };
var x = square(4); // x gets the value 16

然而,函数表达式也可以提供函数名,并且可以用于在函数内部代指其本身,或者在调试器堆栈跟踪中识别该函数:

const factorial = function fac(n) {return n<2 ? 1 : n*fac(n-1)};

console.log(factorial(3));

当将函数作为参数传递给另一个函数时,函数表达式很方便。下面的例子演示了一个叫map的函数如何被定义,而后使用一个表达式函数作为其第一个参数进行调用:

function map(f,a) {
  let result = []; // 创建一个数组
  let i; // 声明一个值,用来循环
  for (i = 0; i != a.length; i++)
    result[i] = f(a[i]);
  return result;
}

下面的代码:

function map(f, a) {
  let result = []; // 创建一个数组
  let i; // 声明一个值,用来循环
  for (i = 0; i != a.length; i++)
    result[i] = f(a[i]);
  return result;
}
const f = function(x) {
   return x * x * x;
}
let numbers = [0,1, 2, 5,10];
let cube = map(f,numbers);
console.log(cube);

返回 [0, 1, 8, 125, 1000]。

调用函数

定义一个函数并不会自动的执行它。定义了函数仅仅是赋予函数以名称并明确函数被调用时该做些什么。调用函数才会以给定的参数真正执行这些动作。例如,一旦你定义了函数square,你可以如下这样调用它:

square(5);

函数一定要处于调用它们的域中,但是函数的声明可以被提升 (出现在调用语句之后),如下例:

console.log(square(5));
/* ... */
function square(n) { return n*n }

函数域是指函数声明时的所在的地方,或者函数在顶层被声明时指整个程序。

备注: 只有使用如上的语法形式(即 function funcName(){})才可以。而下面的代码是无效的。就是说,函数提升仅适用于函数声明,而不适用于函数表达式

console.log(square); // square is hoisted with an initial value undefined.
console.log(square(5)); // Uncaught TypeError: square is not a function
const square = function (n) {
  return n * n;
}

作用域和函数堆栈

递归

一个函数可以指向并调用自身。有三种方法可以达到这个目的:

  1. 函数名
  2. arguments.callee(消除名称紧密耦合的现象,但访问 arguments 是个很昂贵的操作,因为它是个很大的对象,每次递归调用时都需要重新创建。影响现代浏览器的性能,还会影响闭包。,但)
  3. 作用域下的一个指向该函数的变量名

例如,思考一下如下的函数定义:

var foo = function bar() {
   // statements go here
};

在这个函数体内,以下的语句是等价的:

  1. bar()
  2. arguments.callee() (译者注:ES5 禁止在严格模式下使用此属性)
  3. foo()

嵌套函数和闭包(嵌套函数通过调用外部函数指定参数后再调用内部函数和指定参数的方式进行调用)

你可以在一个函数里面嵌套另外一个函数。嵌套(内部)函数对其容器(外部)函数是私有的。它自身也形成了一个闭包。一个闭包是一个可以自己拥有独立的环境与变量的表达式(通常是函数)。

既然嵌套函数是一个闭包,就意味着一个嵌套函数可以”继承“容器函数的参数和变量。换句话说,内部函数包含外部函数的作用域。

可以总结如下:

  • 内部函数只可以在外部函数中访问。
  • 内部函数形成了一个闭包:它可以访问外部函数的参数和变量,但是外部函数却不能使用它的参数和变量。

由于内部函数形成了闭包,因此你可以调用外部函数并为外部函数和内部函数指定参数

function outside(x) {
  function inside(y) {
    return x + y;
  }
  return inside;
}
fn_inside = outside(3); // 可以这样想:给一个函数,使它的值加 3
result = fn_inside(5); // returns 8

result1 = outside(3)(5); // returns 8

命名冲突

当同一个闭包作用域下两个参数或者变量同名时,就会产生命名冲突。更近的作用域有更高的优先权,所以最近的优先级最高,最远的优先级最低。这就是作用域链。链的第一个元素就是最里面的作用域,最后一个元素便是最外层的作用域。

看以下的例子:

function outside() {
  var x = 5;
  function inside(x) {
    return x * 2;
  }
  return inside;
}

outside()(10); // returns 20 instead of 10

命名冲突发生在return x上,inside的参数xoutside变量x发生了冲突。这里的作用链域是{insideoutside, 全局对象}。因此insidex具有最高优先权,返回了 20(insidex)而不是 10(outsidex)。

使用 arguments 对象

arguments
函数接收参数,实际上是接收到一个数组
ECMAScript函数即使定义的函数只接收2个参数,在调用此函数时也不是必须要传2个参数才行,可以传一个、多个甚至是不传。之所以会这样是因为ECMAScript中的参数在内部是用一个数组来表示的;
函数接收到的始终是这个数组,而不关心数组中包含了哪些参数。

函数的实际参数会被保存在一个类似数组的 arguments 对象中。在函数内,你可以按如下方式找出传入的参数:

arguments[i]

其中i是参数的序数编号(译注:数组索引),以 0 开始。所以第一个传来的参数会是arguments[0]。参数的数量由arguments.length表示。

使用 arguments 对象,你可以处理比声明的更多的参数来调用函数。这在你事先不知道会需要将多少参数传递给函数时十分有用。你可以用arguments.length来获得实际传递给函数的参数的数量,然后用arguments对象来取得每个参数。

函数参数

从 ECMAScript 6 开始,有两个新的类型的参数:默认参数,剩余参数。

默认参数

在 JavaScript 中,函数参数的默认值是undefined。然而,在某些情况下设置不同的默认值是有用的。这时默认参数可以提供帮助。

在过去,用于设定默认参数的一般策略是在函数的主体中测试参数值是否为undefined,如果是则赋予这个参数一个默认值。如果在下面的例子中,调用函数时没有实参传递给b,那么它的值就是undefined,于是计算a*b得到、函数返回的是 NaN。但是,在下面的例子中,这个已经被第二行获取处理:

function multiply(a, b) {
  b = (typeof b !== 'undefined') ?  b : 1;

  return a*b;
}

multiply(5); // 5

使用默认参数,在函数体的检查就不再需要了。现在,你可以在函数头简单地把 1 设定为b的默认值:

function multiply(a, b = 1) {
  return a*b;
}

multiply(5); // 5

了解更多默认参数的信息。

剩余参数

剩余参数语法允许将不确定数量的参数表示为数组。在下面的例子中,使用剩余参数收集从第二个到最后参数。然后,我们将这个数组的每一个数与第一个参数相乘。这个例子是使用了一个箭头函数,这将在下一节介绍。

function multiply(multiplier, ...theArgs) {
  return theArgs.map(x => multiplier * x);
}

var arr = multiply(2, 1, 2, 3);
console.log(arr); // [2, 4, 6]

 15.表达式与运算符

运算符

JavaScript 拥有如下类型的运算符。本节描述了运算符和运算符的优先级。

解构

对于更复杂的赋值,解构赋值语法是一个能从数组或对象对应的数组结构或对象字面量里提取数据的 Javascript 表达式。

var foo = ["one", "two", "three"];

// 不使用解构
var one   = foo[0];
var two   = foo[1];
var three = foo[2];

// 使用解构
var [one, two, three] = foo;

位运算符

位运算符将它的操作数视为 32 位元的二进制串(0 和 1 组成)而非十进制八进制或十六进制数。例如:十进制数字 9 用二进制表示为 1001,位运算符就是在这个二进制表示上执行运算,但是返回结果是标准的 JavaScript 数值。

下表总结了 JavaScript 的位运算符。

OperatorUsageDescription
按位与 AND (en-US)a & b在 a,b 的位表示中,每一个对应的位都为 1 则返回 1,否则返回 0.
按位或 OR (en-US)a | b在 a,b 的位表示中,每一个对应的位,只要有一个为 1 则返回 1,否则返回 0.
按位异或 XOR (en-US)a ^ b在 a,b 的位表示中,每一个对应的位,两个不相同则返回 1,相同则返回 0.
按位非 NOT (en-US)~ a反转被操作数的位。
左移 shift (en-US)a << b将 a 的二进制串向左移动 b 位,右边移入 0.
算术右移a >> b把 a 的二进制表示向右移动 b 位,丢弃被移出的所有位。(译注:算术右移左边空出的位是根据最高位是 0 和 1 来进行填充的)
无符号右移 (左边空出位用 0 填充)a >>> b把 a 的二进制表示向右移动 b 位,丢弃被移出的所有位,并把左边空出的位都填充为 0

逻辑运算符

逻辑运算符常用于布尔(逻辑)值之间; 当操作数都是布尔值时,返回值也是布尔值。不过实际上&&||返回的是一个特定的操作数的值,所以当它用于非布尔值的时候,返回值就可能是非布尔值。逻辑运算符的描述如下。

逻辑运算符

运算符范例描述
逻辑与 (en-US) (&&)expr1 && expr2(逻辑与) 如果 expr1 能被转换为 false,那么返回 expr1;否则,返回expr2。因此,&&用于布尔值时,当操作数都为 true 时返回 true;否则返回 false.
逻辑或 (en-US) (||)expr1 || expr2(逻辑或) 如果 expr1 能被转换为 true,那么返回 expr1;否则,返回expr2。因此,|| 用于布尔值时,当任何一个操作数为 true 则返回 true;如果操作数都是 false 则返回 false。
逻辑非 (en-US) (!)!expr(逻辑非) 如果操作数能够转换为 true 则返回 false;否则返回 true。

能被转换为false的值有null0NaN, 空字符串 ("") 和undefined。(译者注:也可以称作”falsy“)

短路求值

作为逻辑表达式进行求值是从左到右,它们是为可能的“短路”的出现而使用以下规则进行测试:

  • false && anything // 被短路求值为 false
  • true || anything // 被短路求值为 true

逻辑的规则,保证这些评估是总是正确的。请注意,上述表达式的anything部分不会被求值,所以这样做不会产生任何副作用。

逗号操作符

逗号操作符,)对两个操作数进行求值并返回最终操作数的值。它常常用在 for 循环中,在每次循环时对多个变量进行更新。

一元操作符

一元操作符仅对应一个操作数。

delete

delete操作符,删除一个对象的属性或者一个数组中某一个键值。语法如下:

delete objectName.property;
delete objectName[index];
delete property; // legal only within a with statement

objectName是一个对象名,property 是一个已经存在的属性,index是数组中的一个已经存在的键值的索引值。

typeof

typeof 操作符 可通过下面 2 种方式使用:

typeof operand
typeof (operand)

typeof 操作符返回一个表示 operand 类型的字符串值。operand 可为字符串、变量、关键词或对象,其类型将被返回。operand 两侧的括号为可选。

void

void 运算符运用方法如下:

void (expression)
void expression

void 运算符,表明一个运算没有返回值。expression 是 javaScript 表达式,括号中的表达式是一个可选项,当然使用该方式是一种好的形式。

你可以使用 void 运算符指明一个超文本链接。该表达式是有效的,但是并不会在当前文档中进行加载。

如下创建了一个超链接文本,当用户单击该文本时,不会有任何效果。

<a href="javascript:void(0)">Click here to do nothing</a>

下面的代码创建了一个超链接,当用户单击它时,提交一个表单。

<a href="javascript:void(document.form.submit())">
Click here to submit</a>

关系运算符

关系运算符对操作数进行比较,根据比较结果真或假,返回相应的布尔值。

in

in操作符,如果所指定的属性确实存在于所指定的对象中,则会返回true,语法如下:

propNameOrNumber in objectName

在这里 propNameOrNumber可以是一个代表着属性名的字符串或者是一个代表着数组索引的数值表达式,而objectName则是一个对象名。

instanceof

如果所判别的对象确实是所指定的类型,则返回true。其语法如下:

objectName instanceof objectType

objectName 是需要做判别的对象的名称,而objectType是假定的对象的类型,例如Date或 Array.

当你需要确认一个对象在运行时的类型时,可使用instanceof. 例如,需要 catch 异常时,你可以针对抛出异常的类型,来做不同的异常处理。

表达式

表达式是一组代码的集合,它返回一个值。(译注:定义比较不好理解,看下面的举例就很好懂了。)

每一个合法的表达式都能计算成某个值,但从概念上讲,有两种类型的表达式:有副作用的(比如赋值)和单纯计算求值的。

表达式 x=7 是第一类型的一个例子。该表达式使用=运算符将值 7 赋予变量 x。这个表达式自己的值等于 7。

代码 3 + 4 是第二个表达式类型的一个例子。该表达式使用 + 运算符把 3 和 4 加到一起但并没有把结果(7)赋值给一个变量。

JavaScript 有以下表达式类型:

  • 算数:得出一个数字,例如 3.14159。(通常使用算数运算符
  • 字符串:得出一个字符串,例如,"Fred" 或 "234"。(通常使用字符串运算符。)
  • 逻辑值:得出 true 或者 false。(经常涉及到逻辑运算符。)
  • 基本表达式:javascript 中基本的关键字和一般表达式。
  • 左值表达式:分配给左值。

基本表达式

this

this关键字被用于指代当前的对象,通常,this指代的是方法中正在被调用的对象。用法如下:

this["propertyName"]
this.propertyName

左值表达式

左值可以作为赋值的目标。

new

你可以使用new 运算符创建一个自定义类型或者是预置类型的对象实例。用法如下:

var objectName = new objectType([param1, param2, ..., paramN]);

super

super 关键字可以用来调用一个对象父类的函数,它在用来调用一个的父类的构造函数时非常有用,比如:

super([arguments]); // calls the parent constructor. super.functionOnParent([arguments]);

16.JavaScript 类型化数组

JavaScript 类型化数组(typed array)是一种类似数组的对象,并提供了一种用于在内存缓冲区中访问原始二进制数据的机制。

JavaScript 类型化数组中的每一个元素都是原始二进制值,而二进制值采用多种支持的格式之一(从 8 位整数到 64 位浮点数)。

但是,不要把类型化数组与普通数组混淆,因为在类型数组上调用 Array.isArray() 会返回 false

缓冲和视图:类型数组架构

为了达到最大的灵活性和效率,JavaScript 类型化数组将实现拆分为缓冲视图两部分。缓冲(由 ArrayBuffer 对象实现)描述的是一个数据分块。缓冲没有格式可言,并且不提供访问其内容的机制。为了访问在缓冲对象中包含的内存,你需要创建一个类型化数组的视图或一个描述缓冲数据格式的 DataView,使用它们来读写缓冲区中的内容。。视图提供了上下文——即数据类型、起始偏移量和元素数——将数据转换为实际有类型的数组。

类型化数组视图

类型化数组视图具有自描述性的名字和所有常用的数值类型像 Int8Uint32Float64 等等。有一种特殊类型的数组 Uint8ClampedArray

DataView

DataView 是一种底层接口,它提供有可以操作缓冲区中任意数据的访问器(getter/setter)API。这对操作不同类型数据的场景很有帮助,例如:类型化数组视图都是运行在本地字节序模式(参考字节序),可以通过使用 DataView 来控制字节序。默认是大端字节序(Big-endian),但可以调用 getter/setter 方法改为小端字节序(Little-endian)。

使用类型化数组的 Web API

下面是一些使用类型化数组的 API 示例,其并不完整,我们会在未来添加更多示例。

FileReader.prototype.readAsArrayBuffer()

FileReader.prototype.readAsArrayBuffer() 读取对应的 Blob 或 File 的内容

XMLHttpRequest.prototype.send()

XMLHttpRequest 实例的 send() 方法现在支持使用类型化数组和 ArrayBuffer 对象作为参数。

ImageData.data

是一个 Uint8ClampedArray 对象,用来描述包含按照 RGBA 序列的颜色数据的一维数组,其值的范围在 0 到 255 之间。

17.JavaScript 模块

我们可以把 export default 放到函数前面,定义它为一个匿名函数,像这样:

export default function(ctx) {
  ...
}

在我们的 main.js 文件中,我们使用以下行导入默认函数:

import randomSquare from './modules/square.js';

同样,没有大括号,因为每个模块只允许有一个默认导出,我们知道 randomSquare 就是需要的那个。上面的那一行相当于下面的缩写:

import {default as randomSquare} from './modules/square.js';

创建模块对象

上面的方法工作的挺好,但是有一点点混乱、亢长。一个更好的解决方是,导入每一个模块功能到一个模块功能对象上。可以使用以下语法形式:

import * as Module from '/modules/module.js';

这将获取 module.js 中所有可用的导出,并使它们可以作为对象模块的成员使用,从而有效地为其提供自己的命名空间。例如:

Module.function1();
Module.function2();

模块与类(class)

正如我们之前提到的那样,您还可以导出和导入类;这是避免代码冲突的另一种选择,如果您已经以面向对象的方式编写了模块代码,那么它尤其有用。

18.从服务器获取数据

Fetch API 的入口点是一个名为 fetch() 的全局函数,它以 URL 为参数;

fetch()是 XMLHttpRequest 的升级版,用于在 JavaScript 脚本里面发出 HTTP 请求。

浏览器原生提供这个对象。

因为 fetch() 返回一个 promise,所以我们将一个函数传递给它返回的 promise 的 then() 方法。此方法会在 HTTP 请求收到服务器的响应时被调用。在它的处理器中,我们检查请求是否成功,并在请求失败时抛出一个错误。否则,我们调用 response.text() 以获取文本形式的响应正文。

XMLHttpRequest API

有时,尤其是在旧的代码中,你会看到另一个名为 XMLHttpRequest(经常会被简写为“XHR”)的 API,它也用于发送 HTTP 请求。其早于 Fetch API,而且是第一个广泛用于实现 AJAX 的 API。

const request = new XMLHttpRequest();

try {
  request.open('GET', 'products.json');

  request.responseType = 'json';

  request.addEventListener('load', () => initialize(request.response));
  request.addEventListener('error', () => console.error('XHR error'));

  request.send();

} catch (error) {
  console.error(`XHR error ${request.status}`);
}

这里有五个阶段:

  1. 创建一个新的 XMLHttpRequest 对象。
  2. 调用它的 open() 以进行初始化。
  3. 为其添加 load 事件的事件监听器,其会在响应加载完成时触发。在监听器中,我们调用 initialize() 函数。
  4. 为其添加 error 事件的事件监听器,其会在请求失败时触发。
  5. 发送请求。

我们还必须将整个事件包装在 try...catch 块中,以便处理 open() 或 send() 可能抛出的错误。

19.客户端存储?

在其他的 MDN 学习中我们已经讨论过 静态网站(static sites)和动态网站( dynamic sites)的区别。大多数现代的 web 站点是动态的— 它们在服务端使用各种类型的数据库来存储数据 (服务端存储), 之后通过运行服务端( server-side)代码来重新获取需要的数据,把其数据插入到静态页面的模板中,并且生成出 HTML 渲染到用户浏览上。

客户端存储以相同的原理工作,但是在使用上有一些不同。它是由 JavaScript APIs 组成的因此允许你在客户端存储数据 (比如在用户的机器上),而且可以在需要的时候重新取得需要的数据。这有很多明显的用处,比如:

  • 个性化网站偏好(比如显示一个用户选择的窗口小部件,颜色主题,或者字体)。
  • 保存之前的站点行为 (比如从先前的 session 中获取购物车中的内容,记住用户是否之前已经登陆过)。
  • 本地化保存数据和静态资源可以使一个站点更快(至少让资源变少)的下载,甚至可以在网络失去链接的时候变得暂时可用。
  • 保存 web 已经生产的文档可以在离线状态下访问。

通常客户端和服务端存储是结合在一起使用的。例如,你可以从数据库中下载一个由网络游戏或音乐播放器应用程序使用的音乐文件,将它们存储在客户端数据库中,并按需要播放它们。用户只需下载音乐文件一次——在随后的访问中,它们将从数据库中检索。

传统方法:cookies

毕竟它过时、存在各种安全问题,而且无法存储复杂数据,而且有更好的、更现代的方法可以在用户的计算机上存储种类更广泛的数据。 cookie 的唯一优势是它们得到了非常旧的浏览器的支持,所以如果您的项目需要支持已经过时的浏览器(比如 Internet Explorer 8 或更早的浏览器),cookie 可能仍然有用,但是对于大多数项目(很明显不包括本站)来说,您不需要再使用它们了。其实 cookie 也没什么好说的,document.cookie一把梭就完事了。 

存储简单数据 — web storage

Web Storage API 非常容易使用 — 你只需存储简单的 键名/键值 对数据 (限制为字符串、数字等类型) 并在需要的时候检索其值。

基本语法

让我们来告诉你怎么做:

  1. 第一步,访问 GitHub 上的 web storage blank template (在新标签页打开此模板)。
  2. 打开你浏览器开发者工具的 JavaScript 控制台。
  3. 你所有的 web storage 数据都包含在浏览器内两个类似于对象的结构中: sessionStorage 和 localStorage。第一种方法,只要浏览器开着,数据就会一直保存 (关闭浏览器时数据会丢失) ,而第二种会一直保存数据,甚至到浏览器关闭又开启后也是这样。我们将在本文中使用第二种方法,因为它通常更有用。
  4. Storage.setItem() 方法允许您在存储中保存一个数据项——它接受两个参数:数据项的名字及其值。试着把它输入到你的 JavaScript 控制台(如果你愿意的话,可以把它的值改为你自己的名字!)
    localStorage.setItem('name','Chris');
    
  5. Storage.getItem() 方法接受一个参数——你想要检索的数据项的名称——并返回数据项的值。现在将这些代码输入到你的 JavaScript 控制台:
    var myName = localStorage.getItem('name');
    myName
    
    在输入第二行时,您应该会看到 myName变量现在包含name数据项的值。
  6. Storage.removeItem() 方法接受一个参数——你想要删除的数据项的名称——并从 web storage 中删除该数据项。在您的 JavaScript 控制台中输入以下几行:
    localStorage.removeItem('name');
    var myName = localStorage.getItem('name');
    myName
    
    第三行现在应该返回 null — name 项已经不存在于 web storage 中。

数据会一直存在!

web storage 的一个关键特性是,数据在不同页面加载时都存在(甚至是当浏览器关闭后,对 localStorage 的而言)。让我们来看看这个:

  1. 再次打开我们的 Web Storage 空白模板,但是这次你要在不同的浏览器中打开这个教程!这样可以更容易处理。
  2. 在浏览器的 JavaScript 控制台中输入这几行:
    localStorage.setItem('name','Chris');
    var myName = localStorage.getItem('name');
    myName
    
    你应该看到 name 数据项返回。
  3. 现在关掉浏览器再把它打开。
  4. 再次输入下面几行:
    var myName = localStorage.getItem('name');
    myName
    
    你应该看到,尽管浏览器已经关闭,然后再次打开,但仍然可以使用该值。

存储复杂数据 — IndexedDB

IndexedDB API(有时简称 IDB)是可以在浏览器中访问的一个完整的数据库系统,在这里,你可以存储复杂的关系数据。其种类不限于像字符串和数字这样的简单值。你可以在一个 IndexedDB 中存储视频,图像和许多其他的内容。

数据库操作需要时间。您不希望在等待结果时挂起浏览器,因此数据库操作是异步的,这意味着它们不会立即发生,而是在将来的某个时刻发生,并且在完成后会收到通知。 要在 IndexedDB 中处理此问题,您需要创建一个请求对象(可以随意命名 - 命名为request,可以表明它的用途)。然后,在请求完成或者失败时,使用事件处理程序来运行代码,您将在下面看到这些代码。

添加数据到数据库

现在让我们看一下如何将记录添加到数据库中。这将使用我们页面上的表单完成。

在你之前的事件处理程序下面,添加以下一行,它设置了一个 submit 事件处理程序,当表单被提交时(当提交 <button> 元素被按下导致表单成功提交),运行一个叫做 addData() 的函数:

// Create an onsubmit handler so that when the form is submitted the addData() function is run
form.onsubmit = addData;

现在让我们定义一下这个addData()功能。在上一行下面添加:

// Define the addData() function
function addData(e) {
  // prevent default - we don't want the form to submit in the conventional way
  e.preventDefault();

  // grab the values entered into the form fields and store them in an object ready for being inserted into the DB
  let newItem = { title: titleInput.value, body: bodyInput.value };

  // open a read/write db transaction, ready for adding the data
  let transaction = db.transaction(['notes'], 'readwrite');

  // call an object store that's already been added to the database
  let objectStore = transaction.objectStore('notes');

  // Make a request to add our newItem object to the object store
  var request = objectStore.add(newItem);
  request.onsuccess = function() {
    // Clear the form, ready for adding the next entry
    titleInput.value = '';
    bodyInput.value = '';
  };

  // Report on the success of the transaction completing, when everything is done
  transaction.oncomplete = function() {
    console.log('Transaction completed: database modification finished.');

    // update the display of data to show the newly added item, by running displayData() again.
    displayData();
  };

  transaction.onerror = function() {
    console.log('Transaction not opened due to error');
  };
}

这很复杂;要打破它,我们:

  • 在事件对象上运行 Event.preventDefault() 以停止以传统方式实际提交的表单(这将导致页面刷新并破坏体验)。
  • 创建一个表示要输入数据库的记录的对象,并使用表单输入中的值填充它。请注意,我们不必明确包含一个 id 值——正如我们提前详细说明的那样,这是自动填充的。
  • 使用 IDBDatabase.transaction() (en-US) 方法打开 notes 对象存储的 readwrite 事务。此事务对象允许我们访问对象存储,以便我们可以对其执行某些操作,例如添加新记录。
  • 使用 IDBTransaction.objectStore() (en-US) 方法访问对象库,将结果保存在 objectStore 变量中。
  • 使用 IDBObjectStore.add() 添加新记录到数据库。这创建了一个请求对象,与我们之前看到的方式相同。
  • 在生命周期的关键点为 request 以及 transaction 对象添加事件处理程序以运行代码。请求成功后,我们会清除表单输入,以便输入下一个注释。交易完成后,我们 displayData() 再次运行该功能以更新页面上的注释显示。

  

离线文件存储

上面的示例已经说明了如何创建一个将大型资产存储在 IndexedDB 数据库中的应用程序,从而无需多次下载它们。这已经是对用户体验的一个很大的改进,但仍然有一件事 - 每次访问网站时仍然需要下载主要的 HTML,CSS 和 JavaScript 文件,这意味着当没有网络连接时,它将无法工作。

这就是服务工作者和密切相关的Cache API 的用武之地。

服务工作者是一个 JavaScript 文件,简单地说,它是在浏览器访问时针对特定来源(网站或某个域的网站的一部分)进行注册的。注册后,它可以控制该来源的可用页面。它通过坐在加载的页面和网络之间以及拦截针对该来源的网络请求来实现这一点。

当它拦截一个请求时,它可以做任何你想做的事情(参见用例思路),但经典的例子是离线保存网络响应,然后提供响应请求而不是来自网络的响应。实际上,它允许您使网站完全脱机工作。

Cache API 是另一种客户端存储机制,略有不同 - 它旨在保存 HTTP 响应,因此与服务工作者一起工作得非常好。

一个 service worker 例子

让我们看一个例子,让你对这可能是什么样子有所了解。我们已经创建了另一个版本的视频存储示例,我们在上一节中看到了 - 这个功能完全相同,只是它还通过服务工作者将 Cache,CSS 和 JavaScript 保存在 Cache API 中,允许示例脱机运行!

请参阅 IndexedDB 视频存储,其中 service worker 在运行,并且还可以查看源代码

注册服务工作者

首先要注意的是,在主 JavaScript 文件中放置了一些额外的代码(请参阅index.js)。首先,我们进行特征检测测试,以查看serviceWorkerNavigator对象中是否有该成员。如果返回 true,那么我们知道至少支持服务工作者的基础知识。在这里,我们使用该ServiceWorkerContainer.register()方法将sw.js文件中包含的服务工作者注册到它所驻留的源,因此它可以控制与它或子目录相同的目录中的页面。当其承诺履行时,服务人员被视为已注册。

  // Register service worker to control making site work offline

  if('serviceWorker' in navigator) {
    navigator.serviceWorker
             .register('/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js')
             .then(function() { console.log('Service Worker Registered'); });
  }

备注: sw.js文件的给定路径是相对于站点源的,而不是包含代码的 JavaScript 文件。服务人员在https://mdn.github.io/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js。原点是https://mdn.github.io,因此给定的路径必须是/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js。如果您想在自己的服务器上托管此示例,则必须相应地更改此示例。这是相当令人困惑的,但出于安全原因,它必须以这种方式工作。

20.重新介绍 JavaScript(JS 教程)

与大多数编程语言不同,JavaScript 没有输入或输出的概念。它是一个在宿主环境(host environment)下运行的脚本语言,任何与外界沟通的机制都是由宿主环境提供的。浏览器是最常见的宿主环境,但在非常多的其他程序中也包含 JavaScript 解释器,如 Adobe Acrobat、Adobe Photoshop、SVG 图像、Yahoo!的 Widget 引擎,Node.js 之类的服务器端环境,NoSQL 数据库(如开源的 Apache CouchDB)、嵌入式计算机,以及包括 GNOME(注:GNU/Linux 上最流行的 GUI 之一)在内的桌面环境等等。

JavaScript 允许你通过任意函数对象的 apply() 方法来传递给它一个数组作为参数列表。

avg.apply(null, [2, 3, 4, 5]); // 3.5

传给 apply() 的第二个参数是一个数组,它将被当作 avg() 的参数列表使用,至于第一个参数 null,我们将在后面讨论。这也正说明了一个事实——函数也是对象。

备注: 通过使用展开语法,你也可以获得同样的效果。比如说:avg(...numbers)

21.自定义对象

使用我们前面讨论过的函数和对象概念,可以像这样完成定义:

function makePerson(first, last) {
    return {
        first: first,
        last: last
    };
}
function personFullName(person) {
    return person.first + ' ' + person.last;
}
function personFullNameReversed(person) {
    return person.last + ', ' + person.first;
}

var s = makePerson('Simon', 'Willison');
personFullName(s); // "Simon Willison"
personFullNameReversed(s); // "Willison, Simon"

上面的写法虽然可以满足要求,但是看起来很麻烦,因为需要在全局命名空间中写很多函数。既然函数本身就是对象,如果需要使一个函数隶属于一个对象,那么不难得到:

function makePerson(first, last) {
    return {
        first: first,
        last: last,
        fullName: function() {
            return this.first + ' ' + this.last;
        },
        fullNameReversed: function() {
            return this.last + ', ' + this.first;
        }
    }
}
s = makePerson("Simon", "Willison");
s.fullName(); // "Simon Willison"
s.fullNameReversed(); // Willison, Simon

上面的代码里有一些我们之前没有见过的东西:关键字 this。当使用在函数中时,this 指代当前的对象,也就是调用了函数的对象。如果在一个对象上使用点或者方括号来访问属性或方法,这个对象就成了 this。如果并没有使用“点”运算符调用某个对象,那么 this 将指向全局对象(global object)。这是一个经常出错的地方。例如:

s = makePerson("Simon", "Willison");
var fullName = s.fullName;
fullName(); // undefined undefined

当我们调用 fullName() 时,this 实际上是指向全局对象的,并没有名为 first 或 last 的全局变量,所以它们两个的返回值都会是 undefined

下面使用关键字 this 改进已有的 makePerson函数:

function Person(first, last) {
    this.first = first;
    this.last = last;
    this.fullName = function() {
        return this.first + ' ' + this.last;
    }
    this.fullNameReversed = function() {
        return this.last + ', ' + this.first;
    }
}
var s = new Person("Simon", "Willison");

我们引入了另外一个关键字:new,它和 this 密切相关。它的作用是创建一个崭新的空对象,然后使用指向那个对象的 this 调用特定的函数。注意,含有 this 的特定函数不会返回任何值,只会修改 this 对象本身。new 关键字将生成的 this 对象返回给调用方,而被 new 调用的函数称为构造函数。习惯的做法是将这些函数的首字母大写,这样用 new 调用他们的时候就容易识别了。

不过,这个改进的函数还是和上一个例子一样,在单独调用fullName() 时,会产生相同的问题。

我们的 Person 对象现在已经相当完善了,但还有一些不太好的地方。每次我们创建一个 Person 对象的时候,我们都在其中创建了两个新的函数对象——如果这个代码可以共享不是更好吗?

function personFullName() {
    return this.first + ' ' + this.last;
}
function personFullNameReversed() {
    return this.last + ', ' + this.first;
}
function Person(first, last) {
    this.first = first;
    this.last = last;
    this.fullName = personFullName;
    this.fullNameReversed = personFullNameReversed;
}

这种写法的好处是,我们只需要创建一次方法函数,在构造函数中引用它们。那是否还有更好的方法呢?答案是肯定的。

function Person(first, last) {
    this.first = first;
    this.last = last;
}
Person.prototype.fullName = function() {
    return this.first + ' ' + this.last;
}
Person.prototype.fullNameReversed = function() {
    return this.last + ', ' + this.first;
}

Person.prototype 是一个可以被 Person 的所有实例共享的对象。它是一个名叫原型链(prototype chain)的查询链的一部分:当你试图访问 Person 某个实例(例如上个例子中的 s)一个没有定义的属性时,解释器会首先检查这个 Person.prototype 来判断是否存在这样一个属性。所以,任何分配给 Person.prototype 的东西对通过 this 对象构造的实例都是可用的。

这个特性功能十分强大,JavaScript 允许你在程序中的任何时候修改原型(prototype)中的一些东西,也就是说你可以在运行时 (runtime) 给已存在的对象添加额外的方法:

s = new Person("Simon", "Willison");
s.firstNameCaps();  // TypeError on line 1: s.firstNameCaps is not a function

Person.prototype.firstNameCaps = function() {
    return this.first.toUpperCase()
}
s.firstNameCaps(); // SIMON

有趣的是,你还可以给 JavaScript 的内置函数原型(prototype)添加东西。让我们给 String 添加一个方法用来返回逆序的字符串:

var s = "Simon";
s.reversed(); // TypeError on line 1: s.reversed is not a function

String.prototype.reversed = function() {
    var r = "";
    for (var i = this.length - 1; i >= 0; i--) {
        r += this[i];
    }
    return r;
}
s.reversed(); // nomiS

定义新方法也可以在字符串字面量上用(string literal)。

"This can now be reversed".reversed(); // desrever eb won nac sihT

正如我前面提到的,原型组成链的一部分。那条链的根节点是 Object.prototype,它包括 toString() 方法——将对象转换成字符串时调用的方法。这对于调试我们的 Person 对象很有用:

var s = new Person("Simon", "Willison");
s; // [object Object]

Person.prototype.toString = function() {
    return '<Person: ' + this.fullName() + '>';
}
s.toString(); // <Person: Simon Willison>

你是否还记得之前我们说的 avg.apply() 中的第一个参数 null?现在我们可以回头看看这个东西了。apply() 的第一个参数应该是一个被当作 this 来看待的对象。下面是一个 new 方法的简单实现:

function trivialNew(constructor, ...args) {
    var o = {}; // 创建一个对象
    constructor.apply(o, args);
    return o;
}

这并不是 new 的完整实现,因为它没有创建原型(prototype)链。想举例说明 new 的实现有些困难,因为你不会经常用到这个,但是适当了解一下还是很有用的。在这一小段代码里,...args(包括省略号)叫作剩余参数(rest arguments)。如名所示,这个东西包含了剩下的参数。

因此,调用

var bill = trivialNew(Person, "William", "Orange");

可认为和调用如下语句是等效的

var bill = new Person("William", "Orange");

apply() 有一个姐妹函数,名叫 call,它也可以允许你设置 this,但它带有一个扩展的参数列表而不是一个数组。

function lastNameCaps() {
    return this.last.toUpperCase();
}
var s = new Person("Simon", "Willison");
lastNameCaps.call(s);
// 和以下方式等价
s.lastNameCaps = lastNameCaps;
s.lastNameCaps();

内部函数

JavaScript 允许在一个函数内部定义函数,这一点我们在之前的 makePerson() 例子中也见过。关于 JavaScript 中的嵌套函数,一个很重要的细节是,它们可以访问父函数作用域中的变量:

function parentFunc() {
  var a = 1;

  function nestedFunc() {
    var b = 4; // parentFunc 无法访问 b
    return a + b;
  }
  return nestedFunc(); // 5
}

如果某个函数依赖于其他的一两个函数,而这一两个函数对你其余的代码没有用处,你可以将它们嵌套在会被调用的那个函数内部,这样做可以减少全局作用域下的函数的数量,这有利于编写易于维护的代码。

这也是一个减少使用全局变量的好方法。当编写复杂代码时,程序员往往试图使用全局变量,将值共享给多个函数,但这样做会使代码很难维护。内部函数可以共享父函数的变量,所以你可以使用这个特性把一些函数捆绑在一起,这样可以有效地防止“污染”你的全局命名空间——你可以称它为“局部全局(local global)”。虽然这种方法应该谨慎使用,但它确实很有用,应该掌握。

闭包

闭包是 JavaScript 中最强大的抽象概念之一——但它也是最容易造成困惑的。它究竟是做什么的呢?代替创建对象来保存状态

function makeAdder(a) {
  return function(b) {
    return a + b;
  }
}
var add5 = makeAdder(5);
var add20 = makeAdder(20);
add5(6); // ?
add20(7); // ?

makeAdder 这个名字本身,便应该能说明函数是用来做什么的:它会用一个参数来创建一个新的“adder”函数,再用另一个参数来调用被创建的函数时,makeAdder 会将一前一后两个参数相加。

从被创建的函数的视角来看的话,这两个参数的来源问题会更显而易见:新函数自带一个参数——在新函数被创建时,便钦定、钦点了前一个参数(如上方代码中的 a、5 和 20,参考 makeAdder 的结构,它应当位于新函数外部);新函数被调用时,又接收了后一个参数(如上方代码中的 b、6 和 7,位于新函数内部)。最终,新函数被调用的时候,前一个参数便会和由外层函数传入的后一个参数相加。

这里发生的事情和前面介绍过的内嵌函数十分相似:一个函数被定义在了另外一个函数的内部,内部函数可以访问外部函数的变量。唯一的不同是,外部函数已经返回了,那么常识告诉我们局部变量“应该”不再存在。但是它们却仍然存在——否则 adder 函数将不能工作。也就是说,这里存在 makeAdder 的局部变量的两个不同的“副本”——一个是 a 等于 5,另一个是 a 等于 20。那些函数的运行结果就如下所示:

add5(6); // 返回 11
add20(7); // 返回 27

下面来说说,到底发生了什么了不得的事情。每当 JavaScript 执行一个函数时,都会创建一个作用域对象(scope object),用来保存在这个函数中创建的局部变量。它使用一切被传入函数的变量进行初始化(初始化后,它包含一切被传入函数的变量)。这与那些保存的所有全局变量和函数的全局对象(global object)相类似,但仍有一些很重要的区别:第一,每次函数被执行的时候,就会创建一个新的,特定的作用域对象;第二,与全局对象(如浏览器的 window 对象)不同的是,你不能从 JavaScript 代码中直接访问作用域对象,也没有 可以遍历当前作用域对象中的属性 的方法。

所以,当调用 makeAdder 时,解释器创建了一个作用域对象,它带有一个属性:a,这个属性被当作参数传入 makeAdder 函数。然后 makeAdder 返回一个新创建的函数(暂记为 adder)。通常,JavaScript 的垃圾回收器会在这时回收 makeAdder 创建的作用域对象(暂记为 b),但是,makeAdder 的返回值,新函数 adder,拥有一个指向作用域对象 b 的引用。最终,作用域对象 b 不会被垃圾回收器回收,直到没有任何引用指向新函数 adder

作用域对象组成了一个名为作用域链(scope chain)的(调用)链。它和 JavaScript 的对象系统使用的原型(prototype)链相类似。

一个闭包,就是 一个函数 与其 被创建时所带有的作用域对象 的组合。闭包允许你保存状态——所以,它们可以用来代替对象。这个 StackOverflow 帖子里有一些关于闭包的详细介绍。

 22.JavaScript 数据类型和数据结构

动态和弱类型

JavaScript 是一种有着动态类型动态语言。JavaScript 中的变量与任何特定值类型没有任何关联,并且任何变量都可以分配(重新分配)所有类型的值:

let foo = 42; // foo 现在是一个数值
foo = "bar"; // foo 现在是一个字符串
foo = true; // foo 现在是一个布尔值

JavaScript 也是一个弱类型语言,这意味着当操作涉及不匹配的类型是否,它将允许隐式类型转换,而不是抛出一个错误。

Symbol 类型

Symbol 是唯一并且不可变的原始值并且可以用来作为对象属性的键(如下)。在某些程序语言当中,Symbol 也被称作“原子类型”(atom)。symbol 的目的是去创建一个唯一属性键,保证不会与其他代码中的键产生冲突。

Object

在计算机科学中,对象(object)是指内存中的可以被标识符引用的一块区域。在 JavaScript 中,对象是唯一可变的值。事实上,函数也是具有额外可调用能力的对象

属性

在 JavaScript 中,对象可以被看作是一组属性的集合。用对象字面量语法来定义一个对象时,会自动初始化一组有限的属性;然后,这些属性还可以被添加和移除。对象属性等价于键值对。属性键要么是字符串类型,要么是 symbol。属性值可以是任何类型的值,包括其他对象,从而可以构建复杂的数据结构。

有两种对象属性的类型:数据属性访问器属性。每个属性都有对应的特性(attribute)。JavaScript 引擎在内部内置了访问性,但是你可以通过 Object.defineProperty() 设置它们,或者通过 Object.getOwnPropertyDescriptor() 读取它们。你可以在 Object.defineProperty() 页面上读取更多有关信息。

数据属性

数据属性将键与值相关联。它可以通过以下属性来描述:

value

通过属性访问器获取值。可以是任意的 JavaScript 值。

writable

一个布尔值,表示是否可以通过赋值来改变属性。

enumerable

一个布尔值,表示是否可以通过 for...in 循环来枚举属性。另请参阅枚举性和属性所有权,以了解枚举属性如何与其他函数和语法交互。

configurable

一个布尔值,表示该属性是否可以删除,是否可以更改为访问器属性,并可以更改其特性。

访问器属性

将键与两个访问器函数(get 和 set)像关联,以获取或者存储值。

备注: 重要的是,意识到它是访问器属性——而不是访问器方法。我们可以将函数作为值来提供给 JavaScript 对象的访问器,使得对象表现得像一个类——但这并不能使该对象成为类。

一个访问器属性有着以下的特性:

get

该函数使用一个空的参数列表,以便有权对值执行访问时,获取属性值。参见 getter。可能是 undefined

set

使用包含分配值的参数调用的函数。每当尝试更改指定属性时执行。参见 setter。可能是 undefined

enumerable

一个布尔值,表示是否可以通过 for...in 循环来枚举属性。另请参阅枚举性和属性所有权,以了解枚举属性如何与其他函数和语法交互。

configurable

一个布尔值,表示该属性是否可以删除,是否可以更改为访问器属性,并可以更改其特性。

对象的原型(prototype)指向另一个对象或者 null——从概念上讲,它是对象的隐藏属性,通常表示为 [[Prototype]]。对象的 [[Prototype]] 属性也可以在对象自身访问。

对象是临时的键值对,因此它们通常当作映射使用。然而,这可能涉及人类工程学、安全以及性能的问题。然而,可以使用 Map 来存储任意的数据。Map 引用更详细地讨论了使用普通对象和使用 map 存储键值之间的利弊。

23.继承与原型链 

 遵循 ECMAScript 标准,符号 someObject.[[Prototype]] 用于标识 someObject 的原型。内部插槽 [[Prototype]] 可以通过 Object.getPrototypeOf() 和 Object.setPrototypeOf() 函数来访问。这个等同于 JavaScript 的非标准但被许多 JavaScript 引擎实现的属性 __proto__ 访问器。为在保持简洁的同时避免混淆,在我们的符号中会避免使用 obj.__proto__,而是使用 obj.[[Prototype]] 作为代替。其对应于 Object.getPrototypeOf(obj)

24.并发模型与事件循环

JavaScript 有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。这个模型与其他语言中的模型截然不同,比如 C 和 Java。

函数调用形成了一个由若干帧组成的栈。

JSCopy to Clipboard

function foo(b) {
  let a = 10;
  return a + b + 11;
}

function bar(x) {
  let y = 3;
  return foo(x * y);
}

console.log(bar(7)); // 返回 42

当调用 bar 时,第一个帧被创建并压入栈中,帧中包含了 bar 的参数和局部变量。当 bar 调用 foo 时,第二个帧被创建并被压入栈中,放在第一个帧之上,帧中包含 foo 的参数和局部变量。当 foo 执行完毕然后返回时,第二个帧就被弹出栈(剩下 bar 函数的调用帧)。当 bar 也执行完毕然后返回时,第一个帧也被弹出,栈就被清空了。

对象被分配在堆中,堆是一个用来表示一大块(通常是非结构化的)内存区域的计算机术语。

队列

一个 JavaScript 运行时包含了一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数。

在 事件循环 期间的某个时刻,运行时会从最先进入队列的消息开始处理队列中的消息。被处理的消息会被移出队列,并作为输入参数来调用与之关联的函数。正如前面所提到的,调用一个函数总是会为其创造一个新的栈帧。

函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个消息(如果还有的话)。

事件循环

之所以称之为 事件循环,是因为它经常按照类似如下的方式来被实现:

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

queue.waitForMessage() 会同步地等待消息到达 (如果当前没有任何消息等待被处理)。

"执行至完成"

每一个消息完整地执行后,其他消息才会被执行。这为程序的分析提供了一些优秀的特性,包括:当一个函数执行时,它不会被抢占,只有在它运行完毕之后才会去运行任何其他的代码,才能修改这个函数操作的数据。这与 C 语言不同,例如,如果函数在线程中运行,它可能在任何位置被终止,然后在另一个线程中运行其他代码。

这个模型的一个缺点在于当一个消息需要太长时间才能处理完毕时,Web 应用程序就无法处理与用户的交互,例如点击或滚动。为了缓解这个问题,浏览器一般会弹出一个“这个脚本运行时间过长”的对话框。一个良好的习惯是缩短单个消息处理时间,并在可能的情况下将一个消息裁剪成多个消息。

添加消息

在浏览器里,每当一个事件发生并且有一个事件监听器绑定在该事件上时,一个消息就会被添加进消息队列。如果没有事件监听器,这个事件将会丢失。所以当一个带有点击事件处理器的元素被点击时,就会像其他事件一样产生一个类似的消息。

函数 setTimeout 接受两个参数:待加入队列的消息和一个时间值(可选,默认为 0)。这个时间值代表了消息被实际加入到队列的最小延迟时间。如果队列中没有其他消息并且栈为空,在这段延迟时间过去之后,消息会被马上处理。但是,如果有其他消息,setTimeout 消息必须等待其他消息处理完。因此第二个参数仅仅表示最少延迟时间,而非确切的等待时间。

下面的例子演示了这个概念(setTimeout 并不会在计时器到期之后直接执行):

const s = new Date().getSeconds();

setTimeout(function() {
  // 输出 "2",表示回调函数并没有在 500 毫秒之后立即执行
  console.log("Ran after " + (new Date().getSeconds() - s) + " seconds");
}, 500);

while(true) {
  if(new Date().getSeconds() - s >= 2) {
    console.log("Good, looped for 2 seconds");
    break;
  }
}

零延迟

零延迟并不意味着回调会立即执行。以 0 为第二参数调用 setTimeout 并不表示在 0 毫秒后就立即调用回调函数。

其等待的时间取决于队列里待处理的消息数量。在下面的例子中,"这是一条消息" 将会在回调获得处理之前输出到控制台,这是因为延迟参数是运行时处理请求所需的最小等待时间,但并不保证是准确的等待时间。

基本上,setTimeout 需要等待当前队列中所有的消息都处理完毕之后才能执行,即使已经超出了由第二参数所指定的时间。

(function() {

  console.log('这是开始');

  setTimeout(function cb() {
    console.log('这是来自第一个回调的消息');
  });

  console.log('这是一条消息');

  setTimeout(function cb1() {
    console.log('这是来自第二个回调的消息');
  }, 0);

  console.log('这是结束');

})();

// "这是开始"
// "这是一条消息"
// "这是结束"
// "这是来自第一个回调的消息"
// "这是来自第二个回调的消息"

多个运行时互相通信

一个 web worker 或者一个跨域的 iframe 都有自己的栈、堆和消息队列。两个不同的运行时只能通过 postMessage 方法进行通信。如果另一个运行时侦听 message 事件,则此方法会向该运行时添加消息。

永不阻塞

JavaScript 的事件循环模型与许多其他语言不同的一个非常有趣的特性是,它永不阻塞。处理 I/O 通常通过事件和回调来执行,所以当一个应用正等待一个 IndexedDB 查询返回或者一个 XHR 请求返回时,它仍然可以处理其他事情,比如用户输入。

由于历史原因有一些例外,如 alert 或者同步 XHR,但应该尽量避免使用它们。注意,例外的例外也是存在的(但通常是实现错误而非其他原因)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值