Skip to content

js入门笔记

参考MDN js教程

基础知识

1. JS API分为两类

  • 浏览器API
  • DOM API:DOM(Document Object Model,文档对象模型),DOM API能通过创建、移除和修改HTML,为页面动态应用新样式等手段来操作HTML和CSS
  • 从服务器获取数据的API:又称Ajax技术,例如XMLHttpRequest和Fetch API
  • 绘图API:Canvas和WebGL API
  • 音视频API:例如HTMLMediaElement,Web Audio API和WebRTC
  • 设备API:例如获取定位、系统通知、设备震动等
  • 客户端存储API:例如Web Storage API简单的键-值存储以及使用IndexedDB API的表数据存储。
  • 第三方API
  • Twitter API、新浪微博API、谷歌地图API、高德地图API

2. 怎样向页面添加js

  • 内部js
    <head> 标签结束前插入<script>标签
  • 外部js
    <script src="script.js" async></script>
  • 内联js(不好的做法)
    <button onclick="createParagraph()">点我呀</button>

注意,html元素是按顺序加载的,为了防止js操作还未加载的元素,js要添加在页面最后。

  • async可以使js和html异步加载,互不影响
    <script async src="js/vendor/jquery.js"></script>
  • defer可以强行让js推迟到html加载完毕后再执行
    <script defer src="js/vendor/jquery.js"></script>

3. 注释

///* */均可

4. 变量

如何定义变量:

  • varletconst
  • 不建议使用varvar可以多次声明同一个变量,还可以把变量的声明放在初始化后面,这会使程序很混乱
  • 变量提升(hoisting),是指所有的变量声明都会被移动到作用域的开头位置
  • const是常量

变量类型:

  • Number
  • String,使用单引号或双引号定义,使用+连接字符串
  • Boolean
  • Array,let myNameArray = ['Chris', 'Bob', 'Jim'];
  • Object,let dog = { name : 'Spot', breed : 'Dalmatian' };
  • 使用typeof查看变量类型,typeof vartypeof(var)

隐式类型转换(js是动态类型语言):

  • 'front'+242242会被转换为'242'

显示类型转换

  • let myString = '123'; let myNum = Number(myString);
  • let myNum = 123; let myString = myNum.toString();

5.运算符

算数运算符:

  • 加、减、乘、除、取余:+、-、*、/、%
  • 幂:**,例如2**5表示2的5次幂,等同于Math.pow(2, 5)
  • 自增和自减运算符:++、--,自增自减运算符和C/C++一样,也分前后
  • 赋值运算符:=、+=、-=、*=、/=
  • 比较运算符:===、!==、==、!=、<、>、<=、>=,==和!=只比较值,===和!==不仅比较值,还比较数据类型

6. 字符串

let browserType = 'mozilla';

browserType.length;
browserType[0];
browserType.indexOf('zilla'); //结果是2,找不到返回-1

//提取字符串
browserType.slice(0,3);
browserType.slice(2); //等于 browserType.slice(2,browserType.length);

//大小写转换
let radData = 'My NaMe Is MuD';
radData.toLowerCase();
radData.toUpperCase();

//替换
browserType = browserType.replace('moz','van');

7. 数组

let myData = 'Manchester,London,Liverpool,Birmingham,Leeds,Carlisle';
let myArray = myData.split(',');      //将字符串拆成数组
let myNewString = myArray.join(',');  //相反的操作

let dogNames = ["Rocket","Flash","Bella","Slugger"];
dogNames.toString();  //相当于不带参数的join

//在结尾添加删除
myArray.push(xxx);
myArray.pop();
//在开头添加删除
myArray.unshift(xxx);
myArray.shift();

8. 条件和循环

//条件
if(cond) {} else{}
switch(var){case val: break; default: break;}
(condition)?run this code : run this code instead
//循环
for(let i = 0;i<5;++i){}
while(cond) {}
do{}while(cond)

9. 函数

//普通函数
function myFunction(msgText, msgType) { return xxx;}
//匿名函数
function {}
myButton.onclick = function() {alert('hello');}

10. 事件

//设置监听器,btn是个按钮,check是个函数
btn.addEventListener('click', check);
btn.removeEventListener('click', bgChange);
//设置click事件的处理函数
button.onclick = function() {xxx}

设置监听器这种方法比使用onclick要好,我们可以使用addEventListener为同一个事件设置不同的处理函数,事件发生时这些函数都会执行,但使用onclick做不到这一点

<!-- 请勿在html元素属性中设置事件处理函数 -->
<button onclick="bgChange()">Press me</button>

事件对象,事件对象e会被自动传递给事件处理函数

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

btn.addEventListener('click', bgChange);

使用事件对象阻止事件默认行为,例如我们可以使用preventDefault阻止表单的提交,即使用户按下按钮,数据也不会被提交

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

事件冒泡和捕获,父元素和子元素都有可能注册了事件,现代浏览器在处理事件时,运行两个不同的阶段 - 捕获阶段和冒泡阶段。

  • 在捕获阶段:
  • 浏览器检查元素的最外层祖先<html>,是否在捕获阶段中注册了一个onclick事件处理程序,如果是,则运行它。
  • 然后,它移动到<html>中单击元素的下一个祖先元素,并执行相同的操作,然后是单击元素再下一个祖先元素,依此类推,直到到达实际点击的元素。
  • 在冒泡阶段,恰恰相反:
  • 浏览器检查实际点击的元素是否在冒泡阶段中注册了一个onclick事件处理程序,如果是,则运行它
  • 然后它移动到下一个直接的祖先元素,并做同样的事情,然后是下一个,等等,直到它到达<html>元素。

在现代浏览器中,默认情况下,所有事件处理程序都在冒泡阶段进行注册

可以使用stopPropagation禁止向上冒泡,只处理当前事件

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

还可以利用事件冒泡将所有子元素的事件都集中到父元素上进行处理。

调试

按F12打开开发者工具,可以在控制台调试js。

一些调试手段

  • 使用typeof获取变量类型
  • 返回值
  • true/false
  • undefined:表示变量不存在(没有定义)
  • null
  • 0
  • NaN

API

DOM API

web页面的主要部分(不是html标签):

  • window:代表浏览器上的标签页。可以用于获取窗口大小、绑定event handler等。
  • navigator:代表浏览器在web上的状态和标识(即user agent),可以用来获取一些信息。
  • document:实际的页面

DOM术语:

  • 元素节点
  • 根节点、子节点、后代节点、父节点、祖先节点、兄弟节点
  • 文本节点

可以用DOM API操作元素,但是最好不要用js操作元素,静态内容应该编写到html中,并且用js生成的内容不容易被爬虫抓取。

以下是常见的元素操作:

//=====
//选择元素
//=====
//使用CSS选择器选择元素(选择第一个匹配到的元素)
Document.querySelector()
//选择每个匹配的元素,保存在一个array中
Document.querySelectorAll()

//旧方法:
//根据ID选择元素
Document.getElementById()
//返回一个数组,包含页面中所有某类型节点。如<p>, <a>。元素类型作为参数传递给函数
var elementRefArray = document.getElementsByTagName('p')

//=====
//节点操作
//=====
var para = document.createElement('p'); //创建
sect.appendChild(para);  //放置

var text = document.createTextNode('xxx'); //创建一个文本节点
var linkPara = document.querySelector('p');//把文本节点放到<p>里面
linkPara.appendChild(text); 

sect.appendChild(linkPara); //直接移动一个段落
Node.cloneNode(); //获得一个节点的副本
Node.removeChild(xxx); //删除一个节点
linkPara.parentNode.removeChild(linkPara);//删除自己

修改css样式的三种方法

//方法1(不推荐)
//使用 Document.stylesheets返回CSSStyleSheet数组,该数组保存了文档的所有样式表。
let css = Document.stylesheets()

//方法2
//在元素内部添加内联样式,修改元素的style属性
para.style.color = 'white';
para.style.backgroundColor = 'black';

//方法3
//给元素指定css类
para.setAttribute('class', 'highlight');

window api

//获取窗口大小(这里获得的窗口大小只是当前大小,浏览器窗口缩放后窗口大小会发生变化)
var WIDTH = window.innerWidth;
var HEIGHT = window.innerHeight;
//窗口调整大小事件
window.onresize = function() {}

从服务器获取数据

这种技术又被称为Ajax(Asynchronous JavaScript and XML),

XHR API

let request = new XMLHttpRequest();
request.open('GET', url);
request.responseType = 'text';  //不必要,XHR默认返回文本
request.onload = function() { //请求返回数据时的操作
  poemDisplay.textContent = request.response;
};
request.send();

fecth API

//response.text()函数会返回一个promise,解析结果文本字符串
fetch(url).then(function(response) {
  response.text().then(function(text) {
    poemDisplay.textContent = text;
  });
});
//另外一种更清晰易懂的写法
fetch(url).then(function(response) {
  return response.text()
}).then(function(text) {
  poemDisplay.textContent = text;
});

第三方API

第一种调用第三方API的方法

<!-- 先引入第三方js库 -->
<script type="text/javascript" src="https://maps.google.com/maps/api/js?key=AIzaSyDDuGt0E5IEGkcE6ZfrKfUtE9Ko_de66pA"></script>

<script>
  // 在js代码中直接使用第三方库中的js API
</script>

第三方API通常会检查调用者的权限,不同的API的权限系统不同,例如上面这个google地图的API,用户要实现在google地图的平台上申请一个密钥,作为引入js库的url参数。

第二种:restful API

通过发HTTP请求调用(XHR或fetch api)。

绘图API

canvas(html元素)、webGL

音视频API

video和audio元素允许我们把视频和音频嵌入到网页当中。

HTMLMediaElement API(html5)

客户端存储

cookies:传统方法,它们是网络上最早最常用的客户端存储形式。 它过时、存在各种安全问题,而且无法存储复杂数据,有更好的、更现代的方法可以在用户的计算机上存储种类更广泛的数据。其实cookie也没什么好说的,document.cookie一把梭就完事了。

两种现代方法:

  • Web Storage API:它提供了一种非常简单的语法,用于存储和检索较小的key-value数据项。
  • IndexedDB API:它为浏览器提供了一个完整的数据库系统来存储复杂的数据。
  • Cache API + Service Worker API:离线文件存储

JS类和对象

1. 直接定义一个对象

//定义对象
let person1 = {
  age : 32,
  name : {
    first : 'Bob',
    last : 'Smith'
  },
  greeting: function() {
    alert('Hi! I\'m ' + this.name + '.');
  }
}
//访问对象
person.age
person.interests[1]
person.bio()
person.name.first
person.name.last
//也可以这样访问,所以对象有时被称为关联数组
person['age'] = 45
person['name']['first']

2. 构造函数

js可以通过构造函数给出类定义

下面的代码虽然只定义了Person类的构造函数,但是可以在后面的代码中直接实例化一个Person对象。

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

var alice = new Person("Alice");
alice.greeting();

3. 使用Object创建对象

//创建一个空对象
var person1 = new Object();

person1.name = 'Chris';
person1['age'] = 38;
person1.greeting = function() {
  alert('Hi! I\'m ' + this.name + '.');
}

//将对象作为参数传给object
var person1 = new Object({
  name : 'Chris',
  age : 38,
  greeting : function() {
    alert('Hi! I\'m ' + this.name + '.');
  }
});

//使用create方法创建现有类的对象
var person2 = Object.create(person1);

4. 原型和原型链

原型(prototype)和原型链(prototype chain)是与类和继承有关的概念。原型记录了类的继承关系和对象与对象的关系。

前文提到,js通过函数定义一个类,可以通过console.log( 函数名.prototype );在控制台查看一个类的原型。

下面查看函数dosomething的原型,可以看到浏览器默认将dosomething视为类的构造函数,并且这个类的原型是Object类。

function doSomething(){}
console.log( doSomething.prototype );
//或
var doSomething = function(){};
console.log( doSomething.prototype );

//控制台输出
{
    constructor: ƒ doSomething(),
    __proto__: {
        constructor: ƒ Object(),
        hasOwnProperty: ƒ hasOwnProperty(),
        isPrototypeOf: ƒ isPrototypeOf(),
        propertyIsEnumerable: ƒ propertyIsEnumerable(),
        toLocaleString: ƒ toLocaleString(),
        toString: ƒ toString(),
        valueOf: ƒ valueOf()
    }
}

dosomething的原型加属性,可以看到它的原型多了一个名为foo的属性

function doSomething(){}
doSomething.prototype.foo = "bar";
console.log( doSomething.prototype );

//控制台输出
{
    foo: "bar",
    constructor: ƒ doSomething(),
    __proto__: {
        constructor: ƒ Object(),
        hasOwnProperty: ƒ hasOwnProperty(),
        isPrototypeOf: ƒ isPrototypeOf(),
        propertyIsEnumerable: ƒ propertyIsEnumerable(),
        toLocaleString: ƒ toLocaleString(),
        toString: ƒ toString(),
        valueOf: ƒ valueOf()
    }
}

实例化一个dosomething的对象,并给这个对象增加一个属性,可以看到这个对象的原型是dosomething

function doSomething(){}
doSomething.prototype.foo = "bar";
var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value"; //增加一个属性
console.log( doSomeInstancing );

//控制台输出
{
    prop: "some value",
    __proto__: {
        foo: "bar",
        constructor: ƒ doSomething(),
        __proto__: {
            constructor: ƒ Object(),
            hasOwnProperty: ƒ hasOwnProperty(),
            isPrototypeOf: ƒ isPrototypeOf(),
            propertyIsEnumerable: ƒ propertyIsEnumerable(),
            toLocaleString: ƒ toLocaleString(),
            toString: ƒ toString(),
            valueOf: ƒ valueOf()
        }
    }
}

如何查看原型

//查看一个对象的原型
//访问对象的__proto__属性会得到类的prototype。
obj.__proto__;
Object.getPrototypeOf(obj); 
//查看一个类的原型
doSomething.prototype

对象动态添加的成员不属于对象的原型的一部分,可以通过下面的方法动态修改类的原型。

doSomething.prototype.foo = "bar";

原型链是由原型构成的继承关系。只有原型中的成员会被继承(即只有定义在类的prototype属性中的成员才能被继承。)

一般只在原型中定义方法,不会在原型中定义变量,因为这么做不太灵活。(可能只有在js里这么做不灵活- -、)。js中常见的对象定义模式是,在构造函数中定义属性(使用this.xxx = xxx)、在prototype属性上定义方法。如此,构造器只包含属性定义,而方法则分装在不同的代码块,代码更具可读性

5. 继承

下面的定义了一个Teacher类,继承自Person类(call函数可以调用一个其他函数,并使用第一个参数指定函数运行在哪个对象上)。如果call函数只有第一个参数,则继承Person类的默认值。

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

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

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

  this.subject = subject;
}

但是这种方法只是简单地将Person类的成员重新定义了一遍,并没有继承到Person类的原型(调用Object.getOwnPropertyNames(Teacher.prototype)只能看到Teacher的构造器,看不到greeting方法),下面的方法可以使Teacher继承到Person的原型

Teacher.prototype = Object.create(Person.prototype);
//光执行上面那句不行,Teacher的构造器也是Person的,调用Object.getOwnPropertyNames(Teacher.prototype)会发现什么都没有
//还得执行下面这句,把构造器换成Tercher自己的
Teacher.prototype.constructor = Teacher;
Object.defineProperty(Teacher.prototype, 'constructor', {
    value: Teacher,
    enumerable: false, // so that it does not appear in 'for in' loop
    writable: true });

6. ECMAScript 2015(ECMAScript 6,ES6)中的类

ES6提供类类似C++和Java的类语法(即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 Teacher extends Person {
  constructor(first, last, age, gender, interests, subject, grade) {
    //必须要先调用super(即调用父类的构造函数),否则this不会被自动初始化
    super(first, last, age, gender, interests);

    this.subject = subject;
    this.grade = grade;
  }
  //定义通过getter和setter使得成员能被访问到
  //(不定义也可以)
  get subject() {
    return this._subject;
  }

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

7. JSON

JSON是一种数据格式。并可以直接被视为一个js对象

//json结构
{
  "squadName" : "Super hero squad",
  "homeTown" : "Metro City",
  "formed" : 2016,
  "secretBase" : "Super tower",
  "active" : true,
  "members" : [
    {
      "name" : "Molecule Man",
      "age" : 29,
      "secretIdentity" : "Dan Jukes",
      "powers" : [
        "Radiation resistance",
        "Turning tiny",
        "Radiation blast"
      ]
    },
    {
      "name" : "Madame Uppercut",
      "age" : 39,
      "secretIdentity" : "Jane Wilson",
      "powers" : [
        "Million tonne punch",
        "Damage resistance",
        "Superhuman reflexes"
      ]
    }
}

使用代码向服务器请求资源

var requestURL = 'https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json';
//XMLHttpRequest(简称XHR),可以通过该对象向服务器请求资源文件
var request = new XMLHttpRequest();
request.open('GET', requestURL);
request.responseType = 'json';  //设定响应类型为json
request.send();
request.onload = function() {
  var superHeroes = request.response; //设置响应类型为json以后,superHeros直接就是一个json
  ...
}

如果发送请求前设置了返回类型为text(即request.responseType = 'text';),要使用parse对response数据反序列化。使用以下方法序列化和反序列化json对象。

//以文本字符串形式接受JSON对象作为参数,并返回相应的对象。
parse()
//接收一个对象作为参数,返回一个对应的JSON字符串。
stringify()

异步编程

一些基本概念:同步/异步、阻塞/非阻塞、线程、进程。

js在浏览器中运行时是单线程的。

我们可以通过使用Web Workers API把一些任务交给一个名为worker的单独的线程,它是一个独立于主线程的后台线程。

web workers的局限性是不能访问DOM,即不能更新UI。并且web worker只是一种非常粗糙的机制,并不能支持真正的异步编程,

1. 传统异步

传统的js使用回调函数实现异步,监听器就是一个js异步的例子,给监听器提供事件对应的回调函数(callback),事件发生后这个回调函数就会被调用。

btn.addEventListener('click', () => {
  alert('You clicked me!');
  let pElem = document.createElement('p');
  pElem.textContent = 'This is a newly-added paragraph.';
  document.body.appendChild(pElem);
});

例如使用XMLHttpRequest配合回调函数加载资源

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

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

  xhr.send();
}

function displayImage(blob) {
  let objectURL = URL.createObjectURL(blob);

  let image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
}

loadAsset('coffee.jpg', 'blob', displayImage);

2. promise

promise是一种新式的js异步代码。例如,fetch()API是一个现代的,更高效的XMLHttpRequest

fetch('products.json').then(function(response) {
  return response.json();
}).then(function(json) {
  products = json;
  initialize();
}).catch(function(err) {
  console.log('Fetch problem: ' + err.message);
}).finally(() => {
  runFinalCode();
});
  • promise:fetch()会返回一个promise,promise是一个表示异步操作成功或失败的对象。
  • then()then()需要一个回调函数,当前一个操作成功时才会执行回调函数,前一个操作的成功结果会作为回调函数的输入。每个then()返回一个promise。
  • catch()catch()用来处理错误(同步的try...catch不能与promise一起工作)
  • finally():不管promise成功或失败最终都会执行的代码

promise相比于传统的callback异步有如下好处:

  • promise能够很容易将连续的几个异步操作链接起来,简化程序逻辑,而callback异步会造成“回调地狱”。
  • promise总是按照顺序调用
  • 错误能够集中处理

进一步理解promise:

let promise = fetch('coffee.jpg');

此时的promise的官方术语叫作pending。当fetch操作成功完成时,会将response作为参数传递给then块的回调函数。

// Fetch promises 不会产生 404 或 500错误,只有在产生像网路故障的情况时才会不工作。我们需要判断 response.ok ,如果是 false,抛出错误

let promise2 = promise.then(response => {
  if (!response.ok) {
    //此处不太懂,这里抛出的异常能直接在catch里捕获吗?
    throw new Error(`HTTP error! status: ${response.status}`);
  } else {
    return response.blob();
  }
});

promise总结

  • 创建promise时,它既不是成功也不是失败状态。这个状态叫作pending(待定)。
  • 当promise返回时,称为 resolved(已解决).
  • 一个成功resolved的promise称为fullfilled(实现)。它返回一个值,可以通过将.then()块链接到promise链的末尾来访问该值。then()块中的执行程序函数将包含promise的返回值。
  • 一个不成功resolved的promise被称为rejected(拒绝)。它返回一个原因(reason),一条错误消息,说明为什么拒绝promise。可以通过将.catch()块链接到promise链的末尾来访问此原因。

响应多个promise。[a,b,c]一个是promise数组,有一个promise被拒绝,then就不能执行。传递给回调函数的参数values也是一个数组,由各个promise的返回数据组成。

let a = fetch(url1);
let b = fetch(url2);
let c = fetch(url3);

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

3. 超时和间隔

下面这些函数设置的异步代码实际上在主线程上运行(在其指定的计时器过去之后)。

  • let myGreeting = setTimeout(fn, 5, ...):在指定的时间后执行一段代码。因为是在主线程内执行,因此实际执行间隔可能比指定时间稍长一点,直到主线程调用栈为空。
  • 参数1:函数
  • 参数2:时间间隔(ms)
  • 更多参数:传递给函数的参数
  • 返回:定时器的标识
  • clearTimeout(myGreeting):清除定时
  • const myInterval = setInterval(displayTime, 1000, ...):以固定的时间间隔,重复运行一段代码。时间间隔是从函数开始执行计算的,即时间间隔会将函数本身的执行时间也算进去。如果想指定函数结束执行到下次开始执行的时间,可以通过递归调用setTimeout实现。
  • clearInterval(myInterval)
  • requestAnimationFrame()setInterval的现代版本。在浏览器下一次重新绘制显示之前执行指定的代码块,从而允许动画在适当的帧率下运行,而不管它在什么环境中运行。它是针对setInterval遇到的问题创建的,比如setInterval并不是针对设备优化的帧率运行,有时会丢帧。还有即使该选项卡不是活动的选项卡或动画滚出页面等问题 。

4.动画(requestAnimationFrame)

绘制动画帧

//例1
function draw() {
   // Drawing code goes here
   requestAnimationFrame(draw);
}

draw();

//例2 requestAnimationFrame会在调用函数的时候将调用时间(timestamp)作为参数传入
let startTime = null;

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

   currentTime = timestamp - startTime;

   // Do something based on current time

   requestAnimationFrame(draw);
}

draw(xxx);

撤销

cancelAnimationFrame(xxx);

5.自定义promise

下面这个例子用promise包装了一个setTimeout,resolve和reject是两个函数,分别用来处理和拒绝promise。(resolve和reject由promise负责实现,promise调用这个函数时将resolve和reject作为参数传入。)

resolve函数的作用是将其参数作为promise实现(fullfilled)时的输出,并将这个输出作为then块的参数,reject的作用是将其参数作为promise拒绝(rejected)时的输出,并将这个输出作为catch块的参数

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

timeoutPromise
.then((message) => {
   alert(message);
})
//或
timeoutPromise.then(alert); //第一种种写法本身有点多此一举,教程里面为了体现出promise能够给then的回调函数传递一个参数才在alert外面又加了一层函数定义

再封装一下,使其能够像调用fetch()那样调用

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(function(){
        resolve(message);
      }, interval);
    }
  });
};

一个真实的promise异步例子(不是用setTimeout模拟的)

//具体的处理工作并不由promise完成,当request成功时,调用request.onsuccess()就能完成promise的实现工作。
function promisifyRequest(request) {
  return new Promise(function(resolve, reject) {
    request.onsuccess = function() {
      resolve(request.result);
    };

    request.onerror = function() {
      reject(request.error);
    };
  });
} 

6. async和await

这是上面的promise写法

fetch('coffee.jpg')
.then(response => response.blob())
.then(myBlob => {
  let objectURL = URL.createObjectURL(myBlob);
  let image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
})
.catch(e => {
  console.log('There has been a problem with your fetch operation: ' + e.message);
});

使用async和await重新编写上面的代码,可以去掉then,使代码更容易理解

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

  let objectURL = URL.createObjectURL(myBlob);
  let image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
}

myFetch()
.catch(e => {
  console.log('There has been a problem with your fetch operation: ' + e.message);
});

混合使用async/await和promise

async function myFetch() {
  let response = await fetch('coffee.jpg');
  return await response.blob(); //这里可能写错了,这里不应该带await
}

myFetch().then((blob) => {
  let objectURL = URL.createObjectURL(blob);
  let image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
});

async/await可以和同步的try...catch一起使用

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

    let objectURL = URL.createObjectURL(myBlob);
    let image = document.createElement('img');
    image.src = objectURL;
    document.body.appendChild(image);
  } catch(e) {
    console.log(e);
  }
}

myFetch();

async/await也可以和promise.all一起使用

async function displayContent() {
  let coffee = fetchAndDecode('coffee.jpg', 'blob');
  let tea = fetchAndDecode('tea.jpg', 'blob');
  let description = fetchAndDecode('description.txt', 'text');

  let values = await Promise.all([coffee, tea, description]);
  ...
}

不带await时返回的是pending状态的promise,带await时返回的是fullfilled状态的promise

async/await的缺陷:await关键字会阻塞后面的代码,promise只能一个一个处理。解决办法,先返回几个pending状态的promise,让它们先同时开始处理,然后再集中await,例如

//将这种写法
async function timeTest() {
  await timeoutPromise(3000);
  await timeoutPromise(3000);
  await timeoutPromise(3000);
}

//改为
async function timeTest() {
  const timeoutPromise1 = timeoutPromise(3000);
  const timeoutPromise2 = timeoutPromise(3000);
  const timeoutPromise3 = timeoutPromise(3000);

  await timeoutPromise1;
  await timeoutPromise2;
  await timeoutPromise3;
}

将类成员定义为async异步函数

class Person {
  //直接返回一个fullfilled状态的promise
  async greeting() {
    return await Promise.resolve(`Hi! I'm ${this.name.first}`);
  };
  ...

//使用方法
let han = new Person();
han.greeting().then(console.log);

模板字符串

参考https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Template_literals

模板字符串(Template literals、Template strings),字符串中可以插入表达式。

语法:模板字符串使用反引号`包裹字符串,使用${expression}`插入表达式。可以在表达式中嵌套模板字符串。

模板字符串中可以直接打回车换行,而不用\n,例如下面的例子最终输出的字符串中是带有换行的

console.log(`string text line 1
string text line 2`);

1. 带标签的模板字符串

带有mytag标签的字符串,实际上是调用了模板函数myTag,它的第一个参数是一个字符串数组,原始字符串被${}分割后剩下的一个个字符串构成了这个数组

var person = 'Mike';
var age = 28;

function myTag(strings, personExp, ageExp) {

  var str0 = strings[0]; // "that "
  var str1 = strings[1]; // " is a "

  // There is technically a string after
  // the final expression (in our example),
  // but it is empty (""), so disregard.
  // var str2 = strings[2];

  var ageStr;
  if (ageExp > 99){
    ageStr = 'centenarian';
  } else {
    ageStr = 'youngster';
  }

  return str0 + personExp + str1 + ageStr;

}

var output = myTag`that ${ person } is a ${ age }`;

console.log(output);
// that Mike is a youngster

2. 访问不经过转义的字符串

//方法1:使用模板函数第一个参数的raw属性
function tag(strings) {
  console.log(strings.raw[0]);
}

tag`string text line 1 \n string text line 2`;
//方法2:调用String.raw方法
var str = String.raw`Hi\n${2+3}!`;
// "Hi\n5!"