Skip to content

Latest commit

 

History

History
972 lines (635 loc) · 29.7 KB

File metadata and controls

972 lines (635 loc) · 29.7 KB

Basic

数据类型和变量

Number

JavaScript不区分整数和浮点数,统一用Number表示,以下都是合法的Number类型:

123; // 整数123
0.456; // 浮点数0.456
1.2345e3; // 科学计数法表示1.2345x1000,等同于1234.5
-99; // 负数
NaN; // NaN表示Not a Number,当无法计算结果时用NaN表示
Infinity; // Infinity表示无限大,当数值超过了JavaScript的Number所能表示的最大值时,就表示为Infinity

字符串

字符串是以单引号'或双引号"括起来的任意文本,比如'abc'"xyz"等等

布尔值

比较运算符

要特别注意相等运算符==。JavaScript在设计时,有两种比较运算符:

第一种是==比较,它会自动转换数据类型再比较,很多时候,会得到非常诡异的结果;

第二种是===比较,它不会自动转换数据类型,如果数据类型不一致,返回false,如果一致,再比较。

由于JavaScript这个设计缺陷,不要使用==比较,始终坚持使用===比较。

另一个例外是NaN这个特殊的Number与所有其他值都不相等,包括它自己:

NaN === NaN; // false

唯一能判断NaN的方法是通过isNaN()函数:

isNaN(NaN); // true

最后要注意浮点数的相等比较:

1 / 3 === (1 - 2 / 3); // false

BigInt

1.BigInt创建:整数后面加n,整数最大值为9007199254740991, 当我们想要表示整数9223372036854775807,可用末尾加n变成BigInt9223372036854775807n来存储;

2.BigInt四则运算,计算结果仍为BigInt类型,注意BigInt不能和Number一起运算;

要精确表示比253还大的整数,可以使用内置的BigInt类型,它的表示方法是在整数后加一个n,例如9223372036854775808n,也可以使用BigInt()把Number和字符串转换成BigInt:

// 使用BigInt:
var bi1 = 9223372036854775807n;
var bi2 = BigInt(12345);
var bi3 = BigInt("0x7fffffffffffffff");
console.log(bi1 === bi2); // false
console.log(bi1 === bi3); // true
console.log(bi1 + bi2);

使用BigInt可以正常进行加减乘除等运算,结果仍然是一个BigInt,但不能把一个BigInt和一个Number放在一起运算:

console.log(1234567n + 3456789n); // OK
console.log(1234567n / 789n); // 1564, 除法运算结果仍然是BigInt
console.log(1234567n % 789n); // 571, 求余
console.log(1234567n + 3456789); // Uncaught TypeError: Cannot mix BigInt and other types

null和undefined

null表示一个“空”的值,它和0以及空字符串''不同,0是一个数值,''表示长度为0的字符串,而null表示“空”。

在其他语言中,也有类似JavaScript的null的表示,例如Java也用null,Swift用nil,Python用None表示。但是,在JavaScript中,还有一个和null类似的undefined,它表示“未定义”。

JavaScript的设计者希望用null表示一个空的值,而undefined表示值未定义。事实证明,这并没有什么卵用,区分两者的意义不大。大多数情况下,我们都应该用nullundefined仅仅在判断函数参数是否传递的情况下有用。

数组

数组是一组按顺序排列的集合,集合的每个值称为元素。JavaScript的数组可以包括任意数据类型。例如:

[1, 2, 3.14, 'Hello', null, true];

对象

JavaScript的对象是一组由键-值组成的无序集合,例如:

var person = {
    name: 'Bob',
    age: 20,
    tags: ['js', 'web', 'mobile'],
    city: 'Beijing',
    hasCar: true,
    zipcode: null
};

JavaScript对象的键都是字符串类型,值可以是任意数据类型。上述person对象一共定义了6个键值对,其中每个键又称为对象的属性,例如,personname属性为'Bob'zipcode属性为null

访问属性

let xiaohong = {
    name: '小红',
    'middle-school': 'No.1 Middle School'
};

xiaohong的属性名middle-school不是一个有效的变量,就需要用''括起来。访问这个属性也无法使用.操作符,必须用['xxx']来访问:

xiaohong['middle-school']; // 'No.1 Middle School'
xiaohong['name']; // '小红'
xiaohong.name; // '小红'

添加/删除属性

let xiaoming = {
    name: '小明'
};
xiaoming.age; // undefined
xiaoming.age = 18; // 新增一个age属性
xiaoming.age; // 18
delete xiaoming.age; // 删除age属性

校验属性

如果我们要检测xiaoming对象是否拥有某一属性,可以用in操作符:

let xiaoming = {
    name: '小明',
    birth: 1990,
    school: 'No.1 Middle School',
    height: 1.70,
    weight: 65,
    score: null
};
'name' in xiaoming; // true
'grade' in xiaoming; // false

不过要小心,如果in判断一个属性存在,这个属性不一定是xiaoming的,它可能是xiaoming继承得到的:

'toString' in xiaoming; // true

因为toString定义在object对象中,而所有对象最终都会在原型链上指向object,所以xiaoming也拥有toString属性。

要判断一个属性是否是xiaoming自身拥有的,而不是继承得到的,可以用hasOwnProperty()方法:

let xiaoming = {
    name: '小明'
};
xiaoming.hasOwnProperty('name'); // true
xiaoming.hasOwnProperty('toString'); // false

Map和Set

Map

// 初始化方式1
let m = new Map([['Michael', 95], ['Bob', 75], ['Tracy', 85]]);
m.get('Michael'); // 95

// 初始化方式2
let m = new Map(); // 空Map
m.set('Adam', 67); // 添加新的key-value
m.set('Bob', 59);
m.has('Adam'); // 是否存在key 'Adam': true
m.get('Adam'); // 67
m.delete('Adam'); // 删除key 'Adam'
m.get('Adam'); // undefined

Set

let s1 = new Set(); // 空Set
let s2 = new Set([1, 2, 3]); // 含1, 2, 3
s.add(4);// 含1, 2, 3,4
s.delete(3);// 含1, 2, 4

iterable

for in & for of

  • for in遍历的是数组的索引(index),更适合遍历对象

  • for of遍历的是数组元素值(value),适用遍历数/数组对象/字符串/map/set等拥有迭代器对象(iterator)的集合,但是不能遍历对象,因为没有迭代器对象,但如果想遍历对象的属性,你可以用for in循环(这也是它的本职工作)或用内建的Object.keys()方法

var myObject={
  a:1,
  b:2,
  c:3
}
for (var key of Object.keys(myObject)) {
  console.log(key + ": " + myObject[key]);
}
//a:1 b:2 c:3

小结

for in遍历的是数组的索引(即键名),而for of遍历的是数组元素值

for in总是得到对象的key或数组、字符串的下标

for of总是得到对象的value或数组、字符串的值

函数

函数定义和调用

function abs(x) {
    if (x >= 0) {
        return x;
    } else {
        return -x;
    }
}

上述abs()函数的定义如下:

  • function指出这是一个函数定义;
  • abs是函数的名称;
  • (x)括号内列出函数的参数,多个参数以,分隔;
  • { ... }之间的代码是函数体,可以包含若干语句,甚至可以没有任何语句。

请注意,函数体内部的语句在执行时,一旦执行到return时,函数就执行完毕,并将结果返回。因此,函数内部通过条件判断和循环可以实现非常复杂的逻辑。

如果没有return语句,函数执行完毕后也会返回结果,只是结果为undefined

由于JavaScript的函数也是一个对象,上述定义的abs()函数实际上是一个函数对象,而函数名abs可以视为指向该函数的变量。

因此,第二种定义函数的方式如下:

let abs = function (x) {
    if (x >= 0) {
        return x;
    } else {
        return -x;
    }
};

方法

basic

let xiaoming = {
    name: '小明',
    birth: 1990,
    age: function () {
        let y = new Date().getFullYear();
        return y - this.birth;
    }
};

xiaoming.age; // function xiaoming.age()
xiaoming.age(); // 今年调用是25,明年调用就变成26了

在一个方法内部,this是一个特殊变量,它始终指向当前对象,也就是xiaoming这个变量。所以,this.birth可以拿到xiaomingbirth属性。

function getAge() {
    let y = new Date().getFullYear();
    return y - this.birth;
}

let xiaoming = {
    name: '小明',
    birth: 1990,
    age: getAge
};

xiaoming.age(); // 25, 正常结果
getAge(); // NaN 

单独调用函数getAge()怎么返回了NaN

JavaScript的函数内部如果调用了this,如果以对象的方法形式调用,比如xiaoming.age(),该函数的this指向被调用的对象,也就是xiaoming,这是符合我们预期的。

如果单独调用函数,比如getAge(),此时,该函数的this指向全局对象,也就是window

let fn = xiaoming.age; // 先拿到xiaoming的age函数
fn(); // NaN

// 等效于
let fn = getAge() {
    let y = new Date().getFullYear();
    return y - this.birth;
}
fn(); // NaN fn() 等效于 getAge(),fn拿到的方法签名

apply

虽然在一个独立的函数调用中,根据是否是strict模式,this指向undefinedwindow,不过,我们还是可以控制this的指向的!

要指定函数的this指向哪个对象,可以用函数本身的apply方法,它接收两个参数,第一个参数就是需要绑定的this变量,第二个参数是Array,表示函数本身的参数。

apply修复getAge()调用:

function getAge() {
    let y = new Date().getFullYear();
    return y - this.birth;
}

let xiaoming = {
    name: '小明',
    birth: 1990,
    age: getAge
};

xiaoming.age(); // 25
getAge.apply(xiaoming, []); // 25, this指向xiaoming, 参数为空

另一个与apply()类似的方法是call(),唯一区别是:

  • apply()把参数打包成Array再传入;
  • call()把参数按顺序传入。

比如调用Math.max(3, 5, 4),分别用apply()call()实现如下:

Math.max.apply(null, [3, 5, 4]); // 5
Math.max.call(null, 3, 5, 4); // 5

对普通函数调用,我们通常把this绑定为null

装饰器

利用apply(),我们还可以动态改变函数的行为。

JavaScript的所有对象都是动态的,即使内置的函数,我们也可以重新指向新的函数。

现在假定我们想统计一下代码一共调用了多少次parseInt(),可以把所有的调用都找出来,然后手动加上count += 1,不过这样做太傻了。最佳方案是用我们自己的函数替换掉默认的parseInt()

高阶函数

map

function pow(x) {
    return x * x;
}

let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
let results = arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
console.log(results);

注意:map()传入的参数是pow,即函数对象本身。

reduce

[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)
let arr = [1, 3, 5, 7, 9];
arr.reduce(function (x, y) {
    return x + y;
}); // 25

如果数组元素只有1个,那么还需要提供一个额外的初始参数以便至少凑够两:

let arr = [123];
arr.reduce(function (x, y) {
    return x + y;
}, 0); // 123

filter

Basic

例如,在一个Array中,删掉偶数,只保留奇数,可以这么写:

let arr = [1, 2, 4, 5, 6, 9, 10, 15];
let r = arr.filter(function (x) {
    return x % 2 !== 0;
});
r; // [1, 5, 9, 15]

把一个Array中的空字符串删掉,可以这么写:

let arr = ['A', '', 'B', null, undefined, 'C', '  '];
let r = arr.filter(function (s) {
    return s && s.trim(); // 注意:IE9以下的版本没有trim()方法
}); // 第一个条件去除null和undefined,第二个条件去除''和'  ';
r; // ['A', 'B', 'C']
回调函数

filter()接收的回调函数,其实可以有多个参数。通常我们仅使用第一个参数,表示Array的某个元素。回调函数还可以接收另外两个参数,表示元素的位置和数组本身:

let arr = ['A', 'B', 'C'];
let r = arr.filter(function (element, index, self) {
    console.log(element); // 依次打印'A', 'B', 'C'
    console.log(index); // 依次打印0, 1, 2
    console.log(self); // self就是变量arr
    return true;
});

利用filter,可以巧妙地去除Array的重复元素:

let
    r,
    arr = ['apple', 'strawberry', 'banana', 'pear', 'apple', 'orange', 'orange', 'strawberry'];

r = arr.filter(function (element, index, self) {
    return self.indexOf(element) === index;
});

console.log(r);

sort

let arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
    return y - x;
}); // [20, 10, 2, 1]

Array

every()

every()方法可以判断数组的所有元素是否满足测试条件。

let arr = ['Apple', 'pear', 'orange'];
console.log(arr.every(function (s) {
    return s.length > 0;
})); // true, 因为每个元素都满足s.length>0

console.log(arr.every(function (s) {
    return s.toLowerCase() === s;
})); // false, 因为不是每个元素都全部是小写
find()

find()方法用于查找符合条件的第一个元素,如果找到了,返回这个元素,否则,返回undefined

let arr = ['Apple', 'pear', 'orange'];
console.log(arr.find(function (s) {
    return s.toLowerCase() === s;
})); // 'pear', 因为pear全部是小写

console.log(arr.find(function (s) {
    return s.toUpperCase() === s;
})); // undefined, 因为没有全部是大写的元素
findIndex()

findindex 丢进去的是一个函数,找满足函数关系的元素。

indexof 丢进去的是要找的元素,直接找元素。

let arr = ['Apple', 'pear', 'orange'];
console.log(arr.findIndex(function (s) {
    return s.toLowerCase() === s;
})); // 1, 因为'pear'的索引是1

console.log(arr.findIndex(function (s) {
    return s.toUpperCase() === s;
})); // -1
forEach()

forEach()map()类似,它也把每个元素依次作用于传入的函数,但不会返回新的数组。forEach()常用于遍历数组,因此,传入的函数不需要返回值:

let arr = ['Apple', 'pear', 'orange'];
arr.forEach(x=>console.log(x)); // 依次打印每个元素

闭包

高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回;

标准对象

Promise异步操作

基本使用

Promise 是一个对象,它代表了一个异步操作的最终完成或者失败;Doc Link

假设现在有一个名为 createAudioFileAsync() 的函数,它接收一些配置和两个回调函数,然后异步地生成音频文件。一个回调函数在文件成功创建时被调用,另一个则在出现异常时被调用。

// 成功的回调函数
function successCallback(result) {
  console.log("音频文件创建成功:" + result);
}

// 失败的回调函数
function failureCallback(error) {
  console.log("音频文件创建失败:" + error);
}

createAudioFileAsync(audioSettings, successCallback, failureCallback);

如果重写 createAudioFileAsync() 为返回 Promise 的形式,你可以把回调函数附加到它上面:

createAudioFileAsync(audioSettings).then(successCallback, failureCallback);

推荐的调用方式

doSomething()
  .then(function (result) {
    return doSomethingElse(result);
  })
  .then(function (newResult) {
    return doThirdThing(newResult);
  })
  .then(function (finalResult) {
    console.log(`得到最终结果:${finalResult}`);
  })
  .catch(failureCallback);

Or,更简洁的箭头函数方式

doSomething()
  .then((result) => doSomethingElse(result))
  .then((newResult) => doThirdThing(newResult))
  .then((finalResult) => {
    console.log(`得到最终结果:${finalResult}`);
  })
  .catch(failureCallback);

**! 备注:**箭头函数表达式可以有隐式返回值;所以,() => x() => { return x; } 的简写。

错误处理

基本使用

doSomething()
  .then((result) => doSomethingElse(result))
  .then((newResult) => doThirdThing(newResult))
  .then((finalResult) => console.log(`得到最终结果:${finalResult}`))
  .catch(failureCallback);

==通常,一遇到异常抛出,浏览器就会顺着 Promise 链寻找下一个 onRejected 失败回调函数或者由 .catch() 指定的回调函数。这和以下同步代码的工作原理(执行过程)非常相似。==

try {
  let result = syncDoSomething();
  let newResult = syncDoSomethingElse(result);
  let finalResult = syncDoThirdThing(newResult);
  console.log(`得到最终结果:${finalResult}`);
} catch (error) {
  failureCallback(error);
}

这种异步代码的对称性在 async/await 语法中达到了极致:

async function foo() {
  try {
    const result = await doSomething();
    const newResult = await doSomethingElse(result);
    const finalResult = await doThirdThing(newResult);
    console.log(`得到最终结果:${finalResult}`);
  } catch (error) {
    failureCallback(error);
  }
}
// MY TEST CASE
promiseMain(){
  this.promiseTest()
      .then(this.promiseTest2, function(){console.log('error')})
      .then(this.promiseTest, function(){console.log('error')});
},
promiseTest() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(this.myResolve());
    }, 1000);
  });
},
promiseTest2() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(this.myResolve2());
    }, 1000);
  });
},
myResolve(){
  console.log('业务处理逻辑-' + count++)
  return 'success'; // 返回一个对象(res) or value(res.data.data)
},
myResolve2(){
  throw new Error('resolve2 error!');
}

// 另一个例子:这里的.then(回调函数)里面传参是一个回调函数,回调函数里用到的值res是前一个函数返回的Promise里的值,.then会在前一个Promise 成功or异常Error的时候 再去执行相应的回调函数;
// 在Promise构造器里,只传一个参数的时候是 执行当前一个Promise执行顺利的时候的继续执行的回调函数
// 在Promise构造器里,传两个参数的时候,前一个是在顺利执行的回调,后一个是在抛出Error的时候执行的回调
getSubscriptionsOnAzureElseProviderApi({provider: this.provider}).then((res) => {
    if (res.data.code === 0) {
      console.log("response result", res.data.data);
	}
})

嵌套

嵌套是一种可以限制 catch 语句的作用域的控制结构写法。明确来说,嵌套的 catch 只会捕获其作用域及以下的错误,而不会捕获链中更高层的错误。如果使用正确,可以实现细粒度的错误恢复。

// 如果doSomethingCritical()异常,则找到外部的catch里的内容
// 如过doSomethingOptional异常,则找到其所在then作用域里promise链下一个catch指定的内容
// 如果doSomethingExtraNice异常,则找到下一个catch
doSomethingCritical()
  .then((result) =>
    doSomethingOptional()
      .then((optionalResult) => doSomethingExtraNice(optionalResult))
      .catch((e) => {}),
  ) // 即便可选操作失败了,也会继续执行
  .then(() => moreCriticalStuff())
  .catch((e) => console.log(`严重失败:${e.message}`));

Promise链条解释

主链条:doSomethingCritical -> moreCriticalStuff

详细链条:doSomethingCritical[doSomethingOptional -> doSomethingExtraNice -> 捕捉此子链的catch]

-> moreCriticalStuff[捕捉主链条的catch]

这个内部的 catch 语句仅能捕获到 doSomethingOptional()doSomethingExtraNice() 的失败,并将该错误与外界屏蔽,之后就恢复到 moreCriticalStuff() 继续执行。值得注意的是,如果 doSomethingCritical() 失败,这个错误仅会被最后的(外部)catch 语句捕获到,并不会被内部 catch 吞掉。

async/await 中,这段代码看起来像这样:

async function main() {
  try {
    const result = await doSomethingCritical();
    try {
      const optionalResult = await doSomethingOptional(result);
      await doSomethingExtraNice(optionalResult);
    } catch (e) {
      // 忽略可选步骤的失败并继续执行。
    }
    await moreCriticalStuff();
  } catch (e) {
    console.error(`严重失败:${e.message}`);
  }
}

**! 备注:**如果没有复杂的错误处理,则很可能不需要嵌套的 then 处理器。相反,可以使用扁平链,将错误处理逻辑放在最后。

Catch的后续链式操作

new Promise((resolve, reject) => {
  console.log("初始化");

  resolve();
})
  .then(() => {
    throw new Error("有哪里不对了");

    console.log("执行「这个」");
  })
  .catch(() => {
    console.log("执行「那个」");
  })
  .then(() => {
    console.log("执行「这个」,无论前面发生了什么");
  });

输出结果如下:

初始化
执行「那个」
执行「这个」,无论前面发生了什么

async/await 中,这段代码看起来像这样:

async function main() {
  try {
    await doSomething();
    throw new Error("有哪里不对了");
    console.log("执行「这个」");
  } catch (e) {
    console.error("执行「那个」");
  }
  console.log("执行「这个」,无论前面发生了什么");
}

当一个 Promise 拒绝事件未被任何处理器处理时,它会冒泡到调用栈的顶部,主机需要将其暴露出来。在 Web 上,当 Promise 被拒绝时,会有下文所述的两个事件之一被派发到全局作用域(通常而言,就是 window;如果是在 web worker 中使用的话,就是 Worker 或者其他基于 worker 的接口)。这两个事件如下所示:

  • rejectionhandled

    当 Promise 被拒绝、并且在 reject 函数处理该拒绝事件之后会派发此事件。

  • unhandledrejection

    当 Promise 被拒绝,但没有提供 reject 函数来处理该拒绝事件时,会派发此事件。

上述两种事件(类型为 PromiseRejectionEvent)都有两个属性,一个是 promise 属性,该属性指向被拒绝的 Promise,另一个是 reason 属性,该属性用来说明 Promise 被拒绝的原因。

因此,我们可以通过以上事件为 Promise 失败时提供补偿处理,也有利于调试 Promise 相关的问题。在每一个上下文中,该处理都是全局的,因此不管源码如何,所有的错误都会在同一个处理函数中被捕捉并处理。

Node.js 中,对拒绝事件的处理稍有不同。你可以通过为 Node.js 的 unhandledRejection 事件添加处理器(注意名称的大小写不同)来捕获未处理的拒绝,就像这样:

JSCopy to Clipboard

process.on("unhandledRejection", (reason, promise) => {
  /* 你可以在这里添加一些代码,以便检查 promise 和 reason */
});

对于 Node.js 来说,为了防止错误被记录到控制台(否则默认会发生),添加 process.on() 监听器就足够了;不需要类似浏览器运行时的 preventDefault() 方法这样的等效操作。

然而,如果你添加了 process.on 监听器,但没有在其中添加代码来处理被拒绝的 Promise,那么它们就会被丢弃,而且不会有任何提示。因此,最好在监听器中添加代码来检查每个被拒绝的 Promise,并确保它们不是由于代码错误而导致的。

组合

有四个组合工具可用来并发异步操作:Promise.all()Promise.allSettled()Promise.any()Promise.race()

Basic

另一方面,Promise 是一种控制反转的形式——API 的实现者不控制回调何时被调用。相反,维护回调队列并决定何时调用回调的工作被委托给了 Promise 的实现者,这样一来,API 的使用者和开发者都会自动获得强大的语义保证,包括:

  • 被添加到 then() 的回调永远不会在 JavaScript 事件循环的当前运行完成之前被调用。
  • 即使异步操作已经完成(成功或失败),在这之后通过 then() 添加的回调函数也会被调用。
  • 通过多次调用 then() 可以添加多个回调函数,它们会按照插入顺序进行执行。

以防万一的提醒:传入 then() 的函数永远不会被同步调用,即使 Promise 已经被解决了(resolved):

Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2

传入 then() 的函数不会立即运行,而是被放入微任务队列中,这意味着它会在稍后运行(仅在创建该函数的函数退出后,且 JavaScript 执行堆栈为空时),也就是在控制权返回事件循环之前。总而言之,不会等待太久:

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

wait().then(() => console.log(4));
Promise.resolve()
  .then(() => console.log(2))
  .then(() => console.log(3));
console.log(1); // 1, 2, 3, 4

Promise 回调被处理为微任务,而 setTimeout() 回调被处理为任务队列。

const promise = new Promise((resolve, reject) => {
  console.log("Promise 执行函数");
  resolve();
}).then((result) => {
  console.log("Promise 回调(.then)");
});

setTimeout(() => {
  console.log("新一轮事件循环:Promise(已完成)", promise);
}, 0);

console.log("Promise(队列中)", promise);

上述代码的输出如下:

Promise 执行函数
Promise(队列中)Promise {<pending>}
Promise 回调(.then)
新一轮事件循环:Promise(已完成)Promise {<fulfilled>}

详见深入:微任务与 Javascript 运行时环境

你可能遇到如下情况:你的一些 Promise 和任务(例如事件或回调)会以不可预测的顺序启动。此时,你或许可以通过使用微任务检查状态或平衡 Promise,并以此有条件地创建 Promise。

如果你认为微任务可能会帮助你解决问题,那么请阅读微任务指南,学习如何用 queueMicrotask() 来将一个函数作为微任务添加到队列中。

元编程

Js模块