跳至主要內容

JavaScript

ourandream大约 37 分钟front-end

对 MDN web Docs 中JSopen in new window学习内容的总结.

base

javascript 是一门解释性的编程语言,它一般用于配合 html,css 动态更新网页的内容.它一般在用户端运行,当然它也可以在服务器端运行.

我们可以使用Application Programming Interfaces (APIs) 来利用 js 实现很多有意思的功能.API 分为browsers APIsthird party APIs,前者为浏览器自带的 API,后者会第三方提供的 API.

一般每一个标签页都会运行在一个单独的环境,于是 js 无法得到其他页的数据.这用于保证数据安全.

类似 css,在 html 中 js 有如下方式引入:

internal:

<script>
  // JavaScript goes here
</script>

external:

<script src="script.js" defer></script>

inline:

<script>
  function createParagraph() {
    let para = document.createElement("p");
    para.textContent = "You clicked the button!";
    document.body.appendChild(para);
  }
</script>
<button onclick="createParagraph()">Click me!</button>

同样的,inline 不被推荐使用.

在 html 中我们有async and defer来控制 js 的执行,async是下载完立刻执行,defer是待所有资源加载完后按 html 中的顺序执行.注意它们仅对 src 指定的 js 文件有效.

js 中的注释和 c++类似:

// I am a comment
/*
  I am also
  a comment
*/

js 代码的每一行都要以分号结尾.

variables

variables,即存放值的容器.在 js 中,我们使用 let 声明变量:

let myName;

赋值:

myName = 3;

声明和初始化一起:

let myDog = 'Rover';

在过去的 js 中,我们使用var来进行let的工作,但var中有一些很奇怪的设计,如它可以在声明前初始化变量,可以多次声明变量,在let中这些操作都被禁止了,这让我们减少潜在的错误.所有尽可能使用let.

js 的变量命名有如下需要注意:

  1. 不要用下划线开头,这在 js 中是有特殊含义的.
  2. 不要用数字开头,这是被禁止的.
  3. 不要用 js 的保留字如let.
  4. js 的变量名大小写敏感,a 和 A 是不同的变量.

js 中的变量的类型是动态的,所以我们可以随时为同一个变量更新一个不同类型的值.它主要有如下的类型:

  • numbers
  • strings
  • arrays
  • Boolean
  • objects

js 中也有常量,用const声明和初始化:

const count = 1;

在初始化后不可再修改,但我们可以改变值的内容:

const bird = { species: "Kestrel" };
bird.species = "Striated Caracara";

如果是 array 我们也可以修改它.

我们应该尽可能使用常量,只在必要时使用变量.

basic math

在 js 中常用的只有一种数字类型Number(实际还有一种少用的BigInt用于大数).它可以进行整数小数的相关计算.

对于一个字符串数字,我们可以用Number(str)调用构造函数将其转化.

基本的加减乘除和幂运算:

OperatorNamePurposeExample
+AdditionAdds two numbers together.6 + 9
-SubtractionSubtracts the right number from the left.20 - 15
*MultiplicationMultiplies two numbers together.3 * 7
/DivisionDivides the left number by the right.10 / 5
%Remainder (sometimes called modulo)Returns the remainder left over after you've divided the left number into a number of integer portions equal to the right number.8 % 3 (returns 2, as three goes into 8 twice, leaving 2 left over).
**ExponentRaises a base number to the exponent power, that is, the base number multiplied by itself, exponent times. It was first Introduced in EcmaScript 2016.5 ** 2 (returns 25, which is the same as 5 * 5).

++,--和 c++中的一样,放变量前面返回加后值,放后面返回原来的值.

赋值的相关运算符也和 c++中的一样:

OperatorNamePurposeExampleShortcut for
+=Addition assignmentAdds the value on the right to the variable value on the left, then returns the new variable valuex += 4;x = x + 4;
-=Subtraction assignmentSubtracts the value on the right from the variable value on the left, and returns the new variable valuex -= 3;x = x - 3;
*=Multiplication assignmentMultiplies the variable value on the left by the value on the right, and returns the new variable valuex *= 3;x = x * 3;
/=Division assignmentDivides the variable value on the left by the value on the right, and returns the new variable valuex /= 5;x = x / 5;

比较的运算符就稍有不同了:

OperatorNamePurposeExample
===Strict equalityTests whether the left and right values are identical to one another5 === 2 + 4
!==Strict-non-equalityTests whether the left and right values are not identical to one another5 !== 2 + 3
<Less thanTests whether the left value is smaller than the right one.10 < 6
>Greater thanTests whether the left value is greater than the right one.10 > 20
<=Less than or equal toTests whether the left value is smaller than or equal to the right one.3 <= 2
>=Greater than or equal toTests whether the left value is greater than or equal to the right one.5 >= 4

注意===!==,实际上 js 是有==!=的,但它们不会检查值的类型,使用前值能更严格地进行比较.

string

js 中的 string 使用单引号或双引号围起来:

const string = "The revolution will not be televised.";

我们应该只使用一种引号,在一个引号里加另一种是允许的:

const dblSgl = "I'm feeling blue.";

但使用同种引号则需要转义字符:

const bigmouth = "I've got no right to take my place...";

如果我们想要连接字符串,直接用加运算符:

greeting + ", " + name;

但我们也可以使用template literal,它和字符串类似,使用**`**包裹,不同的是它可以使用变量:

const greeting = `Hello, ${name}`;

变量如果是数字也会自动转化成字符串.我们也可以添加检查的表达式:

const output = `I like the song ${song}.  ${(score / highestScore) * 100}%.`;

书写多行也只需要直接在源代码换行:

const output = `I like the song.
I gave it a score of 90%.`;

但原来的字符串需要加\n换行.故template literal书写字符串更有可读性.

数字和字符串的简单相互转换如下:

const myNum = Number(myString);
const myString2 = myNum2.toString();

有一些字符串的 methods 需要了解.

得到长度:

browserType.length;

访问某个字符(类似数组):

browserType[0];

检查是否包含某个字串:

browserType.includes("zilla");

得到字符串的某个部分,注意不改变原来的字符串,参数是两个索引,且左闭右开:

browserType.slice(1, 4);

转换大小写,同样不改变原来字符串:

radData.toLowerCase();
radData.toUpperCase();

替换部分字符串,同样不改变而返回新的字符串,前一个参数是要替换的字符,注意有多个匹配的话只替换第一个:

const updated = browserType.replace("moz", "van");

查询某个子串在字符串中第一次出现时的位置(索引):

const semiColon = station.indexOf(";");

利用字符串中的某个分隔符(或分隔字符串)分割字符串形成数组:

let myArray = myData.split(",");

array

js 中的 array 创建如下:

let random = ["tree", 795, [0, 1, 2]];

如上所示,它的内容可以是不同类型的量,甚至是数组本身.

它的长度用length访问:

shopping.length;

使用下标访问和修改数组:

shopping[0] = 1;

查找数组内容:

birds.indexOf("Owl");

在数组的后面添加(push)删除(pop)元素:

myArray.push("Cardiff");
myArray.pop();

在数组的前面添加(unshift)删除(shift)元素:

myArray.unshift("Edinburgh");
myArray.shift();

合并数组:

data_type = data_type.concat(array);

遍历数组:

for (let bird of birds) {
  console.log(bird);
}

如果我们想对数组的每一个元素进行操作并返回新的数组,使用map(),它的参数是一个带一个参数函数:

const doubled = numbers.map(double);

类似的,如果像使用某种条件筛选数组并返回符合条件的数组元素:

const longer = cities.filter(isLong);

如果想将数组转变成字符串:

let myNewString = myArray.join(",");

这样会在数组的元素间自动加上参数的字符串分隔然后形成字符串.

如果我们想让数组元素不分隔:

arr.join("");

我们也可以使用toString()来转换,但这样会自动加,分隔且不可修改. 正确按数字大小排序数组:

weeks.sort(function (a, b) {
  return a - b;
});

building blocks

conditions

js 中的条件写法和 c++的非常类似:

if (choice === "sunny") {
  para.textContent =
    "It is nice and sunny outside today. Wear shorts! Go to the beach, or the park, and get an ice cream.";
} else if (choice === "rainy") {
  para.textContent =
    "Rain is falling outside; take a rain coat and an umbrella, and don't stay out for too long.";
} else if (choice === "snowing") {
  para.textContent =
    "The snow is coming down — it is freezing! Best to stay in with a cup of hot chocolate, or go build a snowman.";
} else if (choice === "overcast") {
  para.textContent =
    "It isn't raining, but the sky is grey and gloomy; it could turn any minute, so take a rain coat just in case.";
} else {
  para.textContent = "";
}

和 c++的一样,现检查if的条件,然后一个个检查else if,如果符合就执行对应的代码块,都不符合就会执行else的代码块.

如果条件是一个变量,则不是false, undefined, null, 0, NaN, ('') 中的值的变量都为 true.

我们可以使用&&,||,!来表示逻辑中的与或非用于构成条件.

js 中同样有switch用于简化一些条件的书写:

switch (expression) {
  case choice1:
    run this code
    break;

  case choice2:
    run this code instead
    break;

  // include as many cases as you like

  default:
    actually, just run this code
}

case的值会被用于检查是否和 expression 的值相同,相同则执行相应代码块.

注意breakdefault,如果没有break则会从该条条件开始执行所有的代码块.default则会在没有条件符合时执行.

loop

首先是一般的for循环:

for (initializer; condition; final - expression) {
  // code to run
}

先给定一个初始条件(通常是定义一个局部变量),然后给定允许的条件,最后再给个会改变变量的表达式.

然后按 initializer-condition-code-final-experssion 的顺序执行直到条件不满足.

while:

initializer;
while (condition) {
  // code to run

  final - expression;
}

类似,只是初始和改变的表达式不再括号中.

do-while:

initializer;
do {
  // code to run

  final - expression;
} while (condition);

while类似,但code会首先被执行一次,然后再进行和while一样的操作.

for..of..

for (const item of array) {
  // code to run
}

用于遍历如数组之类的元素集合.注意不一定要const,不过如果是let,修改该变量不会对原数组产生影响.

除此之外还有break用于跳出循环.continue用于进入下一次循环.

function

函数一般的声明如下:

function random(number) {
  return Math.floor(Math.random() * number);
}

调用:

random(1);
btn.onclick = random; //注意作为参数时不能加括号,不然会执行。

注意调用可在声明之后.参数的个数可以为 0.函数内部也可以调用其他的函数.

在函数外定义的变量称为在global scope,它可以在该文件的任何地方调用.

在函数内定义的变量则在自己的scope里,只能在函数内使用.

我们可以给定默认值让参数可选:

function hello(name = "Chris") {
  console.log(`Hello ${name}!`);
}

这样调用时可以给参数也可以不给.

有时我们需要传函数作为参数,此时可以使用匿名函数(即没有名字的函数):

textBox.addEventListener("keydown", function (event) {
  console.log(`You pressed "${event.key}".`);
});

除了没有名字且不可在声明前调用,和正常的函数一样.

我们可以用另外的方式来声明匿名函数:

textBox.addEventListener("keydown", (event) => {
  console.log(`You pressed "${event.key}".`);
});

如果函数只有一行可以去掉花括号:

textBox.addEventListener("keydown", (event) =>
  console.log(`You pressed "${event.key}".`)
);

如果参数只有一个可以去掉括号:

textBox.addEventListener("keydown", (event) =>
  console.log(`You pressed "${event.key}".`)
);

如果只有一行且为返回值,可以去掉return:

const doubled = originals.map((item) => item * 2);

可以使用 rest parameter 语法使用函数剩下的所有参数:

function myFun(a, b, ...manyMoreArgs) {
  console.log("a", a);
  console.log("b", b);
  console.log("manyMoreArgs", manyMoreArgs);
}

myFun("one", "two", "three", "four", "five", "six");

// a, "one"
// b, "two"
// manyMoreArgs, ["three", "four", "five", "six"] <-- notice it's an array

event

event是在系统中发生的行为或事件.在 web 中,event一般与 element 相对应(如按钮被点击).

event相对应的有event handler,它会在 event 发生时被执行.在js中它是一个函数.

event handler

我们可以利用对应的 properties 来应用event handler:

const btn = document.querySelector("button");

function random(number) {
  return Math.floor(Math.random() * (number + 1));
}

btn.onclick = function () {
  const rndCol =
    "rgb(" + random(255) + "," + random(255) + "," + random(255) + ")";
  document.body.style.backgroundColor = rndCol;
};

我们还可以在 html 中应用:

<button onclick="bgChange()">Press me</button>
<!--在script中写有函数-->
<button onclick="alert('Hello, this is my old-fashioned event handler!');">
  Press me
</button>
<!--直接插入js-->

这样使用难以维护,不推荐.

在现代浏览器我们还可以使用addEventListener:

const btn = document.querySelector("button");

function bgChange() {
  const rndCol =
    "rgb(" + random(255) + "," + random(255) + "," + random(255) + ")";
  document.body.style.backgroundColor = rndCol;
}

btn.addEventListener("click", bgChange);

这样有两个好处:

  1. 可移除event handler.
  2. 可以应用多个.

移除可以调用对应函数:

btn.removeEventListener("click", bgChange);

也可以使用一个额外的控制器:

const controller = new AbortController();
btn.addEventListener(
  "click",
  function () {
    var rndCol =
      "rgb(" + random(255) + "," + random(255) + "," + random(255) + ")";
    document.body.style.backgroundColor = rndCol;
  },
  { signal: controller.signal }
); // pass an AbortSignal to this handler

这样的话调用如下语句时所有相关的event handler都会被移除:

controller.abort(); // removes any/all event handlers associated with this controller

other concept

有时我们会空间event handler带一个event参数,它是一个会自动传进的参数,带有额外信息,可进行一些额外的操作.

function bgChange(e) {
  const rndCol =
    "rgb(" + random(255) + "," + random(255) + "," + random(255) + ")";
  e.target.style.backgroundColor = rndCol;
  console.log(e);
}

我们可以使用e.target访问发出event信号的 element.

有时我们阻止 event 的默认行为(如form中有 submit 会自动发送信息并显示成功):

form.onsubmit = function (e) {
  if (fname.value === "" || lname.value === "") {
    e.preventDefault();
    para.textContent = "You need to fill in both names!";
  }
};

我们还需要理解浏览器关于有父 element 的 element 的event handler的处理.它经历三个阶段:

  1. capturing:从 html 开始,一个个检查有无对应event的 handler 并执行,直到该 element 的直接父 element 停止.
  2. target:检查对应的 element 有无 handler,有就执行.然后检查 event 的bubbling查看是否进行第三阶段.
  3. bubbling:从直接父 element 开始,检查有无注册了的event handler并执行,直到 html.

我们写的函数注册event handler默认是注册到第三阶段.

我们可以控制不执行:

video.onclick = function (e) {
  e.stopPropagation();
  video.play();
};

我们也可以将函数注册到第一阶段,只需将addEventListener的第三个参数设为 true.

我们可以利用这些实现event delegation,即只要某 element 的子 element 发出对应信号就会执行一定操作.

objects

object literal

js 的 objects 指一系列数据和功能的集合.其中数据被称为properties,功能被称为methods.

我们可以使用object literal来定义对象:

const person = {
  name: ["Bob", "Smith"],
  age: 32,
  gender: "male",
  interests: ["music", "skiing"],
  bio: function () {
    alert(
      this.name[0] +
        " " +
        this.name[1] +
        " is " +
        this.age +
        " years old. He likes " +
        this.interests[0] +
        " and " +
        this.interests[1] +
        "."
    );
  },
  greeting: function () {
    alert("Hi! I'm " + this.name[0] + ".");
  },
};

如上所示,使用name:value的格式书写,用逗号分隔,value 可以是量也可以是函数.

调用则可以使用dot notation(即点):

person.interests[1];
person.bio();

此时我们说类型作为namespace,用以调用里面的各种 name.

其中调用 properties 也可以使用bracket notation(即方括号):

person["age"];

后者的好处是可以用变量来调用. 移除 property:

delete obj.property;

对象可以嵌套,调用时多写一层即可:

person.name.first;

此时我们说 name 是sub-namespace.

上面的函数里调用成员时使用了this:

greeting: function() {
  alert('Hi! I\'m ' + this.name.first + '.');
}

this指的就是对象本身,这个用法在用类创建对象时会非常有用.

注意 js 的this和一般编程语言不同,它是根据上下文得到含义的,也就是说,在函数中使用也不会出错:

method 里的 this 返回的是 obj, function 里的 this 非严格模式下返回的是 global or window ,严格模式下返回 undefined, 箭头函数里没有自己的 this.

我们可以修改 properties 的值:

person.age = 3;

为 object 添加特殊的属性:

Object.defineProperty(object1, "property1", {
  value: 42,
  writable: false,
});

我们也用类似的方法可以创建成员:

person["eyes"] = "hazel";

constructor

js 的constructor书写如下:

function Person(name) {
  this.name = name;
  this.greeting = function () {
    alert("Hi! I'm " + this.name + ".");
  };
}

然后调用:

let person1 = new Person("Bob");

这样我们就可以利用它创建对象了.其中this指针在具体对象中指代其本身,new告诉解释器要创建对象.

注意对每一个对象中的函数,它都会新建一个对应的函数.

还有一些其他的可以创建对象的方法:

let person1 = new Object();
person1.name = "Bob";

这样是先利用Object()创建一个新对象然后再给值.

我们可以直接用object literal作为参数:

let person1 = new Object({
  name: "Chris",
  age: 38,
  greeting: function () {
    alert("Hi! I'm " + this.name + ".");
  },
});

我们也可以用已有对象建立新对象:

let copy1 = Object.create(obj); //如果property有对象则会复制reference
var copy2 = JSON.parse(JSON.stringify(obj)); //深度拷贝

objects prototype

js 通过prototype chain实现继承,即每一个构造函数都有一个prototype属性,用于存放可以被继承的属性或函数.当继承时,该属性的内容被继承.一个个对象的对应关系形成链.这样的话新的对象的函数会是指向前面函数的指针,避免了重复创建函数的问题.

在浏览器中,我们可以使用obj.__proto__来查看它继承了的对象.

我们这样添加可继承的事物:

Person.prototype.farewell = function () {
  alert(this.name.first + " has left the building. Bye for now!");
};

注意Object()的相关性质会被自动放入prototype中,其他的则需自己添加.

需要进行一次实例化才能在浏览器中看见对应的prototype.

基于此,我们构造函数的书写一般如下:

// Constructor with property definitions

function Test(a, b, c, d) {
  // property definitions
}

// First method definition

Test.prototype.x = function() { ... };

// Second method definition

Test.prototype.y = function() { ... };

// etc.

注意只要对象的调用在更新之后,它仍会被更新,即使对象的定义在构造函数的更新之前.

上面我们使用的create其实也继承了参数的属性,跟上面是同样机制.

对象也有constructor property:

person1.constructor;

可以用它获取构造函数的名字:

person1.constructor.name;

inheritance

js 中的继承书写如下:

function Teacher(first, last, age, gender, interests, subject) {
  Person.call(this, first, last, age, gender, interests);

  this.subject = subject;
}

这样我们从 Person 的构成函数派生形成了一个新构造函数,注意传入参数里this是必须的.

然后我们可以设置prototype:

Teacher.prototype = Object.create(Person.prototype);

但此时 Teacher 的 constructor property 会指向 Person,为解决这个问题,我们需要进行设置:

Object.defineProperty(Teacher.prototype, "constructor", {
  value: Teacher,
  enumerable: false, // so that it does not appear in 'for in' loop
  writable: true,
});

class

ECMAScript 2015提供了类似 c++语言中的class的写法:

class Person {
  constructor(first, last, age, gender, interests) {
    this.name = {
      first,
      last,
    };
    this.age = age;
    this.gender = gender;
    this.interests = interests;
  }

  greeting() {
    console.log(`Hi! I'm ${this.name.first}`);
  }

  farewell() {
    console.log(`${this.name.first} has left the building. Bye for now!`);
  }
}

上面是class declaration的写法,还要class assignment的写法:

// unnamed
let Rectangle = class {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};
console.log(Rectangle.name);
// output: "Rectangle"

// named
let Rectangle = class Rectangle2 {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};
console.log(Rectangle.name);
// output: "Rectangle2"

在解释器其实会将其转化为上面的构造函数的相关内容.下面的函数会被自动加入prototype中. 在 class 的 body 使用strict mode. 使用static创建 class 的 properties 和 methods,它们只可通过 class 本身调用,不可通过 class instance 调用:

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  static displayName = "Point";
  static distance(a, b) {
    const dx = a.x - b.x;
    const dy = a.y - b.y;

    return Math.hypot(dx, dy);
  }
}

const p1 = new Point(5, 5);
const p2 = new Point(10, 10);
p1.displayName; // undefined
p1.distance; // undefined
p2.displayName; // undefined
p2.distance; // undefined

console.log(Point.displayName); // "Point"
console.log(Point.distance(p1, p2)); // 7.0710678118654755

使用public field declarations定义 properties:

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

#进行 private field declaration,定义的相关内容只可在 class 内访问:

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

派生写法:

class Teacher extends Person {
  constructor(subject, grade) {
    super(); //让this可以,如果Person有参数需要给相应的参数.
    this.subject = subject;
    this.grade = grade;
  }
}

返回继承的 class 的内容:

class Lion extends Cat {
  speak() {
    super.speak();
    console.log(`${this.name} roars.`);
  }
}

使用Mix-ins,进行函数的继承:

let calculatorMixin = (Base) =>
  class extends Base {
    calc() {}
  };

let randomizerMixin = (Base) =>
  class extends Base {
    randomize() {}
  };

//use
class Foo {}
class Bar extends calculatorMixin(randomizerMixin(Foo)) {}

如果后来我们想修改里面的 properties,使用getterssetters:

class Teacher extends Person {
  constructor(first, last, age, gender, interests, subject, grade) {
    super(first, last, age, gender, interests);
    // subject and grade are specific to Teacher
    this._subject = subject;
    this.grade = grade;
  }

  get subject() {
    return this._subject;
  }

  set subject(newSubject) {
    this._subject = newSubject;
  }
}

这样我们就可以使用 subject 修改_subject 了.且我们可以在每次属性被调用或被修改时进行一些操作.

json

json是一类使用 js 中的类写法为格式的文件,用以传递数据.把 json 字符串转化为 js 对象称为deserialization,将 js 对象转化为 json 字符串被称为serialization.json的一般格式如下:

{
  "squadName": "Super hero squad",
  "formed": 2016,
  "active": true,
  "powers": [
    "Immortality",
    "Heat Immunity",
    "Inferno",
    "Teleportation",
    "Interdimensional travel"
  ]
}

有几点需要注意:

  • 字符串必须使用双引号.

  • properties 必须用双引号分隔.

json也可以是数组:

[
	...
]

它甚至可以一个数字,一个字符串.

在 js 中对json的处理使用JSON对象:

let myString = JSON.stringify(myObj); //转化为json
const superHeroes = JSON.parse(superHeroesText); //转化为js对象

asynchronous

在 js 中使用asynchronous可以将费时的工作(如查询数据库)放到另一线程中,避免导致网页加载时间过长.

js 中的异步操作会放在event loop中,它会在非异步操作执行完后执行.js 实际上还是一个单线程语言。

callback

在 js 中我们可以使用Async callbacks,即给定一个函数作为参数,它会在调用的函数执行完毕后执行.

如:

btn.addEventListener("click", () => {
  alert("You clicked me!");

  let pElem = document.createElement("p");
  pElem.textContent = "This is a newly-added paragraph.";
  document.body.appendChild(pElem);
});

function loadAsset(url, type, callback) {
  let xhr = new XMLHttpRequest();
  xhr.open("GET", url);
  xhr.responseType = type;

  xhr.onload = function () {
    callback(xhr.response);
  };

  xhr.send();
}

上述代码中的 callback 会在之后执行,不影响它下面的代码.

注意不是所有的 callback 都是异步的(如forEach).

promise

promise指一个代表函数执行是否成功的中间态对象,它是modern web APIs,用于更好的控制异步操作。

当它被创建但没有执行时,我们说它pending,当它被返回时,我们说它被resolved.成功的叫fullfilled,函数的返回信息可以作为参数,失败的叫rejected,返回错误信息(reason).

我们可以使用then()在那之后执行相关操作,它也返回promise:

fetch("products.json")
  .then(function (response) {
    return response.json();
  })
  .then(function (json) {
    let products = json;
    initialize(products);
  })
  .catch(function (err) {
    console.log("Fetch problem: " + err.message);
  });

fetch本身是一个异步函数,它返回promise对象.

注意它不会对如404之类的网络错误作出响应,我们需要检查response:

let promise2 = promise.then((response) => {
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  } else {
    return response.blob();
  }
});

注意try..catch对它不起作用,需要使用特定的catch.

它相对于 callback 有以下好处:

  • 可以使用多个then进行多步异步操作,这在 callback 中很难实现( callback hellopen in new window).
  • 可以让代码运行顺序准确.
  • 更容易处理异常.
  • 不丢失相关操作的控制权.(callback 使用第三方库会导致).

如果我们想对多个promise同时进行相应:

Promise.all([a, b, c]).then(values => {
  ...
});

此时all会返回一个promise,如果成功的话还会有一个数组作为参数.

注意如果 all 中的参数某一个出现问题,它仍会fullfill,只该参数返回undefined.

如果我们想让某些代码无论成功失败都指向,在最后使用finally:

myPromise
  .then((response) => {
    doSomething(response);
  })
  .catch((e) => {
    returnError(e);
  })
  .finally(() => {
    runFinalCode();
  });

我们可以使用promise constructor来让一些老的异步 API(如setTImeout)可以使用promise:

let timeoutPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Success!");
  }, 2000);
});

此时不可能出现reject,有无setTimeout没有失败状态.

如果我们想使用reject:

function timeoutPromise(message, interval) {
  return new Promise((resolve, reject) => {
    if (message === "" || typeof message !== "string") {
      reject("Message is empty or not a string");
    } else if (interval < 0 || typeof interval !== "number") {
      reject("Interval is negative or not a number");
    } else {
      setTimeout(() => {
        resolve(message);
      }, interval);
    }
  });
}

timeouts and internals

我们可以使用setTimeout让某个函数在另一个线程等待一定的时间后运行:

let myGreeting = setTimeout(() => {
  alert("Hello, Mr. Universe!");
}, 2000);

第一个参数是函数,第二个参数是等待时间(毫秒).它的返回值则可以用于控制停止执行:

clearTimeout(myGreeting);

类似的,我们使用setTimeinternal来让某个函数没间隔一段时间就运行一次:

const myInterval = setInterval(myFunction, 2000);
clearInterval(myInterval);

实际上我们也可以使用setTimeout来实现间隔循环运行:

let i = 1;

setTimeout(function run() {
  console.log(i);
  i++;
  setTimeout(run, 100);
}, 100);

这与setInternal有些许不同.setInternal的间隔时间包括执行时间,而setTimeout不包括,也就是说后者可以保证某个确定的时间间隔.

当我们将setTimeout的时间设为 0 时,它仍会等待主线程的非异步操作结束后再执行.

类似的还有requestAnimationFrame:

rAF = requestAnimationFrame(draw);
cancelAnimationFrame(rAF);

它会在网页每次画面刷新时执行,不需要我们给定时间参数.它会尽可能让自己运行得快并接近 60 帧.它一般绘制动画.

我们可以添加一个参数得到它的时间:

let startTime = null;

function draw(timestamp) {
  if (!startTime) {
    startTime = timestamp;
  }

  currentTime = timestamp - startTime;

  // Do something based on current time

  requestAnimationFrame(draw);
}

draw();

上述函数均在主线程运行.

async and await

我们可以使用async让一个函数或类的方法返回promise:

async function hello() {
  return "Hello";
}

这样它会返回promise且它的返回值会在then中作为参数.

在有async的函数中,我们可以使用await来省略then:

async function myFetch() {
  let response = await fetch('coffee.jpg');
...
}

这样response会等到 fetch 执行完成后才会得到返回值,它会直接是该函数的返回值而不是promise.

如果使用了await省略then,我们可以直接使用 try..catch 来处理异常:

async function myFetch() {
  try {
    let response = await fetch("coffee.jpg");
  } catch (e) {
    console.log(e);
  }
}

awaitpromiseall等函数仍适用.

正常情况下如果我们使用多个await,它们会等待一个执行完后再执行另一个:

async function timeTest() {
  await timeoutPromise(3000);
  await timeoutPromise(3000);
  await timeoutPromise(3000);
}

但我们可以让它们同时执行:

async function timeTest() {
  const timeoutPromiseResolve1 = timeoutPromiseResolve(5000);
  const timeoutPromiseReject2 = timeoutPromiseReject(2000);
  const timeoutPromiseResolve3 = timeoutPromiseResolve(3000);

  const results = await Promise.all([
    timeoutPromiseResolve1,
    timeoutPromiseReject2,
    timeoutPromiseResolve3,
  ]);
  return results;
}

注意最后利用all是为了方便异常处理.

APIs

document

就总体而言.

window对象代表网页所在的标签页.

Navigator对象代表浏览器的状态特征.

Document对象指具体加载的网页,用 DOM 代表.

document 的相关 API 主要用于操作DOM.

利用 css 的 selector 选择 element:

const link = document.querySelector("a"); //选择第一个出现的element.
const arr = document.querySelectorAll("a"); //选择所有element并放回类似数组的对象.

较老的选择方法:

const elementRef = document.getElementById("myId"); //利用id选择
const elementRefArray = document.getElementsByTagName("p"); //选择某个特定tag,返回类似数组的对象.

创建 element:

const para = document.createElement("p");

修改属性:

link.textContent = "Mozilla Developer Network";

使用classList修改 class 的列表(注意它本身是只读的):

// If the control is not active there is nothing to do
if (!select.classList.contains("active")) return;

// We need to get the list of options for the custom control
var optList = select.querySelector(".optList");

// We close the list of option
optList.classList.add("hidden");

// and we deactivate the custom control itself
select.classList.remove("active");

加入某个 element 的最后:

sect.appendChild(para);

注意如果是已有的 element 则它会被移动到最后,不会产生新的 element.如果想复制用Node.cloneNode().

删除某个 element:

sect.removeChild(linkPara); //删除子element.
linkPara.remove(); //删除本身,就浏览器不支持.

修改 css:

para.style.color = "white";
para.style.backgroundColor = "black";

注意 css 中用连字符熟悉的 attribute 需要使用小驼峰方式重新书写.以上语句会变成添加到inline css中.

设置attribute:

para.setAttribute("class", "highlight");

设置tabIndex(即按 tab foucus 到的顺序):

element.tabIndex = index;
var index = element.tabIndex;

它的值是一个正整数或 0,它的顺序的规则:

  1. 是如果设置了则按设置的数的大小排序,若同样大按出现的顺序排序.
  2. 如果为 0 或不支持,则按出现的次序排序.

fetch data from server

在以前,我们使用XMLHttpRequest来请求数据(Ajax),首先,我们需要创建一个对象:

let request = new XMLHttpRequest();

指定链接和 http 请求类型:

request.open("GET", url);

指定响应的类型:

request.responseType = "text";

指定得到响应时的 callback:

request.onload = function () {
  poemDisplay.textContent = request.response;
};

最后发出请求:

request.send();

我们现在可以使用更方便的fetch:

fetch(url)
  .then(function (response) {
    return response.text();
  })
  .then(function (text) {
    poemDisplay.textContent = text;
  });

fetch(url, {
  method: "post",
  body: data,
});

它是一个异步函数,返回promise,而且我们不需要在处理请求类型和发送请求.

当是如图片之类的文件时,我们可以通过blob处理成二进制文件,然后生成临时链接;

fetch(url)
  .then(function (response) {
    return response.blob();
  })
  .then(function (blob) {
    // Convert the blob to an object URL — this is basically a temporary internal URL
    // that points to an object stored inside the browser
    let objectURL = URL.createObjectURL(blob);
    // invoke showProduct
    showProduct(objectURL, product);
  });

third-party APIs

要使用第三方的 API,有时我们需要在 html 进行连接:

<script src="https://api.mqcdn.com/sdk/mapquest-js/v1.3.2/mapquest.js"></script>
<link
  type="text/css"
  rel="stylesheet"
  href="https://api.mqcdn.com/sdk/mapquest-js/v1.3.2/mapquest.css"
/>

然后再在 js 中使用.

有时我们直接通过 http 请求得到数据.

一般它们都需要 API keys 才能使用,这是 API 提供商为避免滥用而设立的.

drawing graphic

我们可以在 html 中创建canvas,然后在 js 中画图.

首先选择并定义它的长宽并保存:

const canvas = document.querySelector(".myCanvas");
const width = (canvas.width = window.innerWidth);
const height = (canvas.height = window.innerHeight);

然后获取作图区域(context):

const ctx = canvas.getContext("2d");

填充背景色:

ctx.fillStyle = "rgb(0, 0, 0)";
ctx.fillRect(0, 0, width, height);

第二个函数实际是画一个矩形并用颜色填充,头两个参数是矩形的左上角所在位置,后两个参数为宽度和高度.canvas的坐标默认以左上为(0,0)以保证全体坐标都为正.

我们可以改变原点位置:

ctx.translate(width / 2, height / 2);

旋转整个canvas(弧度制):

ctx.rotate(degToRad(5));

注意上述颜色可以是透明的:

ctx.fillStyle = "rgba(255, 0, 255, 0.75)";

如果我们想画边框图而不是填充全部:

ctx.strokeStyle = "rgb(255, 255, 255)";
ctx.lineWidth = 5;
ctx.strokeRect(25, 25, 175, 200);

参数的意义同上.其中lineWidth用于设置边框宽度.

我们还可以使用路径来画图.

ctx.fillStyle = "rgb(255, 0, 0)";
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(150, 50);
ctx.lineTo(100, 50);
ctx.lineTo(50, 50);
ctx.fill();

其中beginPath为开始路径画图(0,0),moveTo是移动而不画线,lineTo则画线,最后将路径围成的图像进行填充.

我们可以不画线而画圆:

ctx.arc(150, 106, 50, degToRad(0), degToRad(360), false);

前两个参数为圆的中心点,第三个参数为半径,第四个和第五个参数为起始角度和结束角度(弧度制),最后选择顺时针逆时针(false 为逆时针)。0度方向为水平向右.

注意如果路径没有形成封闭图片,浏览器会自动在起点和终点补充连线形成封闭图像,所以想画不完整的圆需要画到圆心的直线.

我们也可以利用stroke来画只有边线的图像。

我们还可以画字:

ctx.strokeStyle = "white";
ctx.lineWidth = 1;
ctx.font = "36px arial";
ctx.strokeText("Canvas text", 50, 50);

ctx.fillStyle = "red";
ctx.font = "48px georgia";
ctx.fillText("Canvas text", 50, 150);

font的设置和 css 中的相同,最后画图的函数的后两个参数指定文本框的左下角.

我们还可以添加图片:

let image = new Image();
image.src = "firefox.png";
image.onload = function () {
  ctx.drawImage(image, 20, 20, 185, 175, 50, 50, 185, 175);
};

drawImage的第一个参数为图像 element,第二和第三个参数指定裁剪图像的左上位置,第四第五则指定裁剪图像的宽度和高度,6,7 指定画图位置的左上角,8,9 为画图的宽度和高度.

另外我们可以使用requestAnimationFrame等函数绘制动画,注意每一次我们都需要刷新画图并画新的图像,我们无法控制已画的图像.

我们还可以使用webGL画 3d 图像,通常我们会使用第三方库(如 three.js)来简化这一操作.

video and audio

我们可以使用相关的 api 自己写一个播放控件,具体方法是在 html 搭好框架后再在 js 中写相关的功能.它的图标可以使用 web icon font 以便于切换.

首先我们需要选定 element:

const media = document.querySelector("video");

下面的 APIs 负责视频开始和暂停:

media.play();
media.pause();

想要复原的话暂停的基础上设置时间为 0:

media.currentTime = 0;

该 property 的单位是秒.

如果想加速减速播放,可使用setTimeInternal在暂停的基础上来让currentTime增加减少.

media.duration可获得整个视频的时长(秒),用于进度条相关控件的实现.

client-side storage

我们可以在用户端存储一些数据便于加快网页加载,防止多次下载同样的内容.

在过去我们使用cookies来存储,现在我们使用 Web Storage 和 IndexDB 来存储,前者用于存储简单的数据类似(如字符串,数字),后者用于存储复杂的数据类型(如视频).

它们会根据不同的网站生成分离的数据库.

Web Storage我们可以使用sessionStorage(仅在标签页加载时临时存储)和localStorage(即使标签页关闭也存在).它们的用法类似.其中的数据以name:value的格式组织.

添加数据:

localStorage.setItem("name", "Chris");

读取数据:

let myName = localStorage.getItem("name");

IndexDB的操作就复杂得多了.

首先我们需要创建一个变量指向数据库:

// Create an instance of a db object for us to store the open database in
let db;

然后添加请求并得到数据库:

window.onload = function () {
  // Open our database; it is created if it doesn't already exist
  // (see onupgradeneeded below)
  let request = window.indexedDB.open("notes_db", 1);
  // onerror handler signifies that the database didn't open successfully
  request.onerror = function () {
    console.log("Database failed to open");
  };

  // onsuccess handler signifies that the database opened successfully
  request.onsuccess = function () {
    console.log("Database opened successfully");

    // Store the opened database object in the db variable. This is used a lot below
    db = request.result;

    // Run the displayData() function to display the notes already in the IDB
    displayData();
  };
};

注意open的第一个参数为数据库名,第二个为版本号.

当数据库不存在或无该高版本时,会触发onupgradeneeded:

// Setup the database tables if this has not already been done
request.onupgradeneeded = function (e) {
  // Grab a reference to the opened database
  let db = e.target.result;

  // Create an objectStore to store our notes in (basically like a single table)
  // including a auto-incrementing key
  let objectStore = db.createObjectStore("notes_os", {
    keyPath: "id",
    autoIncrement: true,
  });

  // Define what data items the objectStore will contain
  objectStore.createIndex("title", "title", { unique: false });
  objectStore.createIndex("body", "body", { unique: false });

  console.log("Database setup complete");
};

里面我们创建了一个表并创建了 index.表的元素会自动被分配一个id.

添加新数据:

let newItem = { title: titleInput.value, body: bodyInput.value };

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

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

// Make a request to add our newItem object to the object store
let request = objectStore.add(newItem);

遍历:

  let objectStore = db.transaction('notes_os').objectStore('notes_os');
  objectStore.openCursor().onsuccess = function(e) {
    // Get a reference to the cursor
    let cursor = e.target.result;

    // If there is still another data item to iterate through, keep running this code
    if(cursor) {
      ...
      // Iterate to the next item in the cursor
      cursor.continue();
    }
     else{
         //当指针为空时进行处理
     }

注意此时需要把 id 进行存储方便删除.

删除:

// Define the deleteItem() function
function deleteItem(e) {
  // retrieve the name of the task we want to delete. We need
  // to convert it to a number before trying it use it with IDB; IDB key
  // values are type-sensitive.
  let noteId = Number(e.target.parentNode.getAttribute('data-note-id'));

  // open a database transaction and delete the task, finding it using the id we retrieved above
  let transaction = db.transaction(['notes_os'], 'readwrite');
  let objectStore = transaction.objectStore('notes_os');
  let request = objectStore.delete(noteId);

  // report that the data item has been deleted

上述操作仍需要加载 html,css,js 等文件,不可离线,我们也可以利用service worker来实现离线可访问.

service worker指一个被注册的为某个网站处理请求的 js 文件.它可以存下 http 请求的内容.

注册:

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

注意路径是相对于网站跟路径而言的.

安装(即网站加载时开始控制请求):

self.addEventListener("install", function (e) {
  e.waitUntil(
    caches.open("video-store").then(function (cache) {
      return cache.addAll([
        "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/",
        "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/index.html",
        "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/index.js",
        "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/style.css",
      ]);
    })
  );
});

里面用了几个 cache API,open指打开存储的缓存(通过新建对象),addALL指请求相关的内容并加入缓存中.

处理请求,有缓存返回缓存,没缓存进行网络请求:

self.addEventListener("fetch", function (e) {
  console.log(e.request.url);
  e.respondWith(
    caches.match(e.request).then(function (response) {
      return response || fetch(e.request);
    })
  );
});

Validating forms

js 提供了Constraint Validation APIopen in new window用于验证数据.下列 element 支持它们:

sending form data

使用fetchFormData:

let form = document.querySelector("form");

form.addEventListener("submit", async (e) => {
  // on form submission, prevent default

  e.preventDefault();

  // construct a FormData object, which fires the formdata event

  data = new FormData(form);

  await fetch(url + "input_a_bill", {
    method: "post",

    body: data,
  });
});

即使有文件它也会自动处理.

others

Destructuring assignment

用于解包数组或对象的语法. 数组的基础用法:

const [red, yellow, green] = foo;
console.log(red); // "one"
console.log(yellow); // "two"
console.log(green); // "three"

定义声明分开:

let a, b;

[a, b] = [1, 2];
console.log(a); // 1
console.log(b); // 2

长度超过数组会被赋值为 undefined. 可以设置默认值:

let a, b;

[a = 5, b = 7] = [1];
console.log(a); // 1
console.log(b); // 7

理由这种语法可以进行值的交换:

let a = 1;
let b = 3;

[a, b] = [b, a];
console.log(a); // 3
console.log(b); // 1

const arr = [1, 2, 3];
[arr[2], arr[1]] = [arr[1], arr[2]];
console.log(arr); // [1,3,2]

可以处理函数的返回值:

function f() {
  return [1, 2];
}

let a, b;
[a, b] = f();
console.log(a); // 1
console.log(b); // 2

可以忽略掉一些值:

function f() {
  return [1, 2, 3];
}

const [a, , b] = f();
console.log(a); // 1
console.log(b); // 3

const [c] = f();
console.log(c); // 1

将剩下的值赋给一个变量:

const [a, ...b] = [1, 2, 3];
console.log(a); // 1
console.log(b); // [2, 3]

对象基本语法:

const user = {
  id: 42,
  isVerified: true,
};

const { id, isVerified } = user;

console.log(id); // 42
console.log(isVerified); // true

于定义分开(()是必须的):

let a, b;

({ a, b } = { a: 1, b: 2 });

使用新的名字:

const o = { p: 42, q: true };
const { p: foo, q: bar } = o;

console.log(foo); // 42
console.log(bar); // true

默认值:

const { a = 10, b = 5 } = { a: 3 };

console.log(a); // 3
console.log(b); // 5

用于函数参数:

const user = {
  id: 42,
  displayName: "jdoe",
  fullName: {
    firstName: "John",
    lastName: "Doe",
  },
};

function userId({ id }) {
  return id;
}

function whois({ displayName, fullName: { firstName: name } }) {
  return `${displayName} is ${name}`;
}

console.log(userId(user)); // 42
console.log(whois(user)); // "jdoe is John"

设置参数的默认值(使得该函数可不带参数):

function drawChart({
  size = "big",
  coords = { x: 0, y: 0 },
  radius = 25,
} = {}) {
  console.log(size, coords, radius);
  // do some chart drawing
}

drawChart({
  coords: { x: 18, y: 30 },
  radius: 30,
});

数组和对象的 destruction 混用:

const metadata = {
  title: "Scratchpad",
  translations: [
    {
      locale: "de",
      localization_tags: [],
      last_edit: "2014-04-14T08:43:37",
      url: "/de/docs/Tools/Scratchpad",
      title: "JavaScript-Umgebung",
    },
  ],
  url: "/en-US/docs/Tools/Scratchpad",
};

let {
  title: englishTitle, // rename
  translations: [
    {
      title: localeTitle, // rename
    },
  ],
} = metadata;

console.log(englishTitle); // "Scratchpad"
console.log(localeTitle); // "JavaScript-Umgebung"

在循环中使用:

const people = [
  {
    name: "Mike Smith",
    family: {
      mother: "Jane Smith",
      father: "Harry Smith",
      sister: "Samantha Smith",
    },
    age: 35,
  },
  {
    name: "Tom Jones",
    family: {
      mother: "Norah Jones",
      father: "Richard Jones",
      brother: "Howard Jones",
    },
    age: 25,
  },
];

for (const {
  name: n,
  family: { father: f },
} of people) {
  console.log("Name: " + n + ", Father: " + f);
}

// "Name: Mike Smith, Father: Harry Smith"
// "Name: Tom Jones, Father: Richard Jones"

使用 computed property name:

let key = "z";
let { [key]: foo } = { z: "bar" };

console.log(foo); // "bar"

将剩下的赋给一个变量:

let { a, b, ...rest } = { a: 10, b: 20, c: 30, d: 40 };
a; // 10
b; // 20
rest; // { c: 30, d: 40 }

destruction 可以让不合法的 identifier 合法:

const foo = { "fizz-buzz": true };
const { "fizz-buzz": fizzBuzz } = foo;

console.log(fizzBuzz); // true

当进行 destruction 时,js 会检查 prototype 链:

let obj = { self: "123" };
obj.__proto__.prot = "456";
const { self, prot } = obj;
// self "123"
// prot "456" (Access to the prototype chain)

modules

以前 js 通常只需要单文件运行,故没有 module 功能,现代浏览器为起添加了此功能。 注意只有在 modules 中可以使用 modules,故引入文件需:

<script type="module" src="main.js"></script>

modules 的核心的importexport. 首先是export,声明 moudule 可以被 import 的事物,可以是 functions, var, let, const, 或 classes:

export const name = "square";

注意必须是top-level item,故不能在函数内 export. 更场景的用法是在文件结尾一次性 export:

export { name, draw, reportArea, reportPerimeter };

我们还可以设置一个默认 export:

export default function(ctx) {
  ...
}

然后是import:

import { name, draw, reportArea, reportPerimeter } from "./modules/square.js";

import 默认:

import defaultExport from "module-name";
import { default as randomSquare } from "./modules/square.js";

只执行代码不使用:

import "/modules/my-module.js";

其中./表示当前路径.如果写/前缀则需补全前面的文件夹名. import 后不可修改,但可以修改 properties,类似 const. module 和通常的 js 文件有以下不同:

  • 必须使用 server,使用本地文件运行会发送 CORS 错误.
  • 使用strict mode.
  • 自动 defer.
  • 引入几次都只执行一次.
  • import 进的功能在 console 不可用. 我们可以使用as(import,export 均可)来重命名避免命名冲突:
// inside module.js
export { function1 as newFunctionName, function2 as anotherNewFunctionName };

// inside main.js
import { newFunctionName, anotherNewFunctionName } from "./modules/module.js";

我们还可以创建一个 module 对象来方便访问:

import * as Module from "./modules/module.js";
Module.function1();

我们可以阻止多个文件作为一个 module 被import:

export { Square } from "./shapes/square.js";
export { Triangle } from "./shapes/triangle.js";
export { Circle } from "./shapes/circle.js";

注意在其中不能写代码,js 会直接定位到相应的文件. 我们还可以使用import()来动态引入.它返回promise:

import("./modules/myModule.js").then((module) => {
  // Do something with the module.
});

在 module 中可以使用await,引入的文件会自动等待:

// fetch request
const colors = fetch("../data/colors.json").then((response) => response.json());

export default await colors;

常见问题:

  • .js文件必须正确配置MIME-type.
  • 本地运行会导致 CORS 错误.
  • 如果使用.mjs表示 module,有很多环境会不支持.

animation

控制 animation event:

.slidein {
  animation-duration: 3s;
  animation-name: slidein;
  animation-iteration-count: 3;
  animation-direction: alternate;
}

@keyframes slidein {
  from {
    margin-left: 100%;
    width: 300%;
  }

  to {
    margin-left: 0%;
    width: 100%;
  }
}
var element = document.getElementById("watchme");
element.addEventListener("animationstart", listener, false);
element.addEventListener("animationend", listener, false);
element.addEventListener("animationiteration", listener, false);

element.className = "slidein";

注意我们是在 js 中让动画开始(添加 class),不如 start 事件会在 js 执行前发出.