叫我欧文就好

叫我欧文就好 查看完整档案

广州编辑  |  填写毕业院校广州凡岛网络有限公司  |  web前端开发 编辑 zbw-zbw.github.io/kyriewen-blog/ 编辑
编辑

记录一个前端菜鸟的进阶之路...

个人动态

叫我欧文就好 发布了文章 · 4月13日

10道JS高频面试题(重要)

1. 为什么 typeof null === 'object' ?

typeof null // 'object'

由于 JavaScript 中,一个变量的值会被保存在一个 32 位的内存单元中。该单元包含一个 1 或 3 位的类型标志实际数据的值。类型标志存储在单元的最后

  • 000:object - 对象
  • 1:int - 整数
  • 010:double - 浮点数
  • 100:string - 字符串
  • 110:boolean - 布尔值
  • undefined -2^30
  • null 空指针(全是 0)

结果很明显,由于 null 的存储单元(全是 0)最后三位和 object 完全一样是 000

2. 等式 0.1 + 0.2 === 0.3 不成立?

0.1 + 0.2 === 0.3 // false

由于二进制浮点数中的 0.1 和 0.2 并不是十分精确,在两数相加时,会先转换成二进制,0.1 和 0.2 转换成二进制的时候尾数会发生无限循环,然后进行对阶运算,JS 引擎对二进制进行截断,所以造成精度丢失。所以它们相加的结果不是刚好等于 0.3,而是一个非常非常非常接近的数字:0.300000000000000004,所以条件判断为 false。

3. a==1 && a==2 && a==3 成立?

// 方法1
var a = {
  value: 1,
  valueOf: function () {
    return this.value++;
  }
};

// 方法2
var a = {
  value: 1,
  toString: function () {
    return this.value++;
  }
};

// 方法3
var value = 1;
Object.defineProperty(window, "a", {
  get: function () {
    return this.value++;
  }
});

if (a === 1 && a === 2 && a === 3) {
  console.log("这也太神奇了吧!")
}

方法一、二:利用 JS 对象有 toString() 和 valueOf() 两个方法,toString()将该对象的原始值以字符串的形式返回,valueOf()返回最适合该对象的原始值

1.用运算符对对象进行转换的时候 valueOf()的优先级高于 toString()

2.对对象进行强字符串转换时会优先调用 toString()

3.toString()方法不能对 null 和 undefined 进行字符串转换,可以用 String()方法代替

方法三:使用 Object.defineProperty()劫持变量 a,在 get 中返回变量 a++的值。

4. 字符串反转('zbw'->'wbz')?

var name = 'zbw'
var res
res = name.split('').reverse().join('')

console.log(res) // 'wbz'

先把字符串转为数组,然后调用数组的 reverse 方法反转数组元素,最后把数组转回字符串即可

5. 函数每秒依次输出 1,2,3,4,5...9?

for(var i = 0; i < 10; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

期望:每秒依次打印 1、2、3、4、5...9

结果:每秒打印的都是 10

  1. 利用 IIFE
for(var i = 0; i < 10; i++) {
  (function(i) {
    setTimeout(function timer() {
        console.log(i)
    }, i * 1000)
  })(i)
}
  1. let 关键字
for(var i = 0; i < 10; i++) {
  let j = i // 闭包的块作用域
  setTimeout(function timer() {
    console.log(j)
  }, j * 1000)
}
  1. let 关键字(推荐写法)
for(let i = 0; i < 10; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

6. 说一说 this 的指向问题?

要判断一个运行中的函数的 this 绑定,需要找到该函数的调用位置(结合调用栈),接着根据优先级得出的四条规则来判断 this 的绑定对象。

  1. 函数由 new 调用?绑定到新创建的对象
  2. 由 call/apply/bind 调用?绑定到指定对象
  3. 由上下文对象调用?绑定上下文对象
  4. 默认:严格模式下绑定到 undefined,否则绑定到全局对象

ES6 的箭头函数不适用以上四条规则,而是根据当前的词法作用域来决定 this 绑定,也就是说,箭头函数会继承外层函数调用的 this 绑定(无论绑定到什么),而且箭头函数的 this 绑定无法被修改

7. JavaScript 的数据类型?

number、string、boolean、undefined、null、object、symbol、bigInt

其中除了 object 以外,其他的类型统称为基本数据类型

object 类型称为引用数据类型(复杂数据类型),它包含两个子类型(array、function)

8. Symbol 类型有什么作用?

  1. 可以用来表示一个独一无二的变量,防止命名冲突。
  2. 可以用来模拟私有变量。(利用 symbol 不会被常规的方法(除了 Object.getOwnPropertySymbols 外)遍历)
  3. 主要用来提供遍历接口,布置了 symbol.iterator 的对象才可以使用 for···of 循环,可统一处理数据结构。

9. NaN 是什么?typeof NaN 输出?

NaN(not a number)不是一个数字,但 typeof NaN 输出 'number'。换句话说,NaN 可以理解为不是数字的数字(虽然有点绕口)。

10. JavaScript 的隐式转换?

一般情况下,非基本数据类型的数据会 优先调用 valueOf() 来获取基本数据类型的值,如果无法获取则继续调用 toString() 获取基本数据类型的值。

  • 字符串和数字相加

如果有一个为字符串,那么都转化为字符串然后执行字符串拼接

'11' + 23 + '24' // 112324
  • 字符串和数字相减

转化为数字再进行运算

'11' - 2 // 9
  • 布尔值和数字

转化为数字再进行运算

1 + true // 2

1 + false // 1
  • 其他类型和布尔类型

将布尔类型转化为数字再进行运算

  • 对象和非对象

执行对象 ToPrimitive(valueOf/toString)然后再进行比较

查看原文

赞 0 收藏 0 评论 0

叫我欧文就好 发布了文章 · 4月12日

深入理解JavaScript中的值(array、string、number...)

数组(array)

JavaScript 中,数组可以容纳任何类型的值

  • 多维数组
var a = [1, '2', [3]]

a.length // 3
a[0] // 1
a[2][0] // 3

注意:使用 delete 可以删除数组中的元素,但不会改变数组的 length 属性

  • 稀疏数组
  • 数组的索引可以是数字,可以是字符串
var a = []
a[0] = 1;
a['demo'] = 2

a.length // 思考一下:这里为什么是1,不是2
a['demo'] // 2
a.demo // 2

注意:数组中的字符串键值和属性不会计算在数组的长度中。

var a = []
a['11'] = 23 // 11会被当成索引,不会存储为key

a.length // 12

注意:如果数组的索引是能够被转为数字的话,它会被当成数字索引。

  • 类数组

一组通过数字索引的值(如 arguments,DOM 元素列表...)

类数组转换为数组(slice)

function demo() {
  var args = arguments
  var arr = Array.prototype.slice.call(args)
  arr.push('zbw')
  console.log(arr)
}

demo('kyrie', 'wen') // ['kyrie', 'wen', 'zbw']

ES6 的 Array.from(...)也可以实现转换

var arr = Array.from(arguments)

字符串(string)

字符串经常被当成字符串数组。

var a = 'wen'
var b = ['w', 'e', 'n']

字符串也是类数组,也有 length 属性,也可以调用数组的方法(indexof,concat...)

var a = 'wen'
var b = ['w', 'e', 'n']

a.indexof('e') // 1
b.indexof('e') // 1

var c = a.concat('wen') // 'wenwen'
var d = b.concat(['w', 'e', 'n']) // ['w', 'e', 'n', 'w', 'e', 'n']
  • 字符串和数组的区别

字符串是不可变的,而数组是可变的。

字符串不可变是指它的成员函数不会改变原始值,而是创建一个新的字符串。

数组的成员函数是在原始值上进行操作。

var a = 'wen'

b = a.toUpperCase()
a // 'wen'
b // 'WEN'
a === b // false

另一个不同点是字符串反转(面试题)。数组有一个成员函数 reverse(字符串没有)

// 字符串反转
var a = 'kyrie'

var res = a.split().reverse().join()
console.log(res) // 'eiryk'

字符串反转:先将字符串转为数组(spilit),然后将数组反转(reverse),最后再转回字符串(join)。

数字(number)

number:唯一的数值类型(整数和小数)

其实,JavaScript 没有真正意义上的整数,怎么理解?

整数就是没有小数的十进制数,JavaScript 中,11.0 等同于 11

var num1 = 11
var num2 = 11.23

// 数字前、后的0都可省略
var num3 = 0.11
var num4 = .11

var num5 = 11.00
var num6 = 11. // 一般不这么写,代码可读性直接 42
  • 指定小数部分的显示位数(toFixed)
var num = 11.23

num.toFixed(0) // 11
num.toFixed(1) // 11.2
num.toFixed(2) // 11.23
num.toFixed(3) // 11.230

注意:如果指定的小数部分的显示位数大于实际位数,会用 0 补齐。

  • toFixed 也适用于数字常量
// 报错
11.toFixed(2)

// 不会报错 正常
(11).toFixed(2) // 11.00
0.11.toFixed(1) // 0.1
11 .toFixed(3) // 11.000 不建议 多个空格很奇怪
11..toFixed(3) // 11.000 不建议 2个.也很奇怪

注意:42.toFixed(2)是无效语法,因为.会被当作常量 42.的一部分,所以没有属性访问运算符来调用 toFixed 方法。(所以再加一个.就好了)

  • 较小的数值

二进制浮点数最大的问题(经典面试题)

0.1 + 0.2 === 0.3 // false  what???

从数学角度来说,上面的条件判断应该为 true,可结果为什么是 false 呢?

简单来说,**二进制浮点数中的 0.1 和 0.2 并不是十分精确,它们相加的结果并不是刚好等于
0.3,而是一个比较接近的数字 0.30000000000000004**,所以条件判断结果为 false。

怎样判断 0.1 + 0.2 和 0.3 是否相等?

最常见的方法是设置一个误差范围值,通常称为“机器精度”。对于 JavaScript 来说,这个值通常为 2^-52

从 ES6 开始,该值定义在 Number.EPSILON 中,直接使用:

function numToEqual(num1, num2) {
  return Math.abs(num1 - num2) < Number.EPSILON;
}

var a = 0.1 + 0.2;
var b = 0.3;

numToEqual(a, b) // true

特殊数值

  • 不是值的值

undefined 类型只有一个值,undefined,null 类型也是只有一个值,null,它们的名称既是类型也是值。

  • nul 和 undefined 的区别

    • null 曾经赋过值,但目前没有值
    • undefined 从未赋值

null 是一个关键字,不是标识符,不能用作变量名称。

undefined 是标识符,可当作变量来使用

var null = 11 // 非法

var undefined = 11 // 合法,但千万别这样做

注意:永远不要重新定义 undefined

  • void 运算符

undefined 是一个内置标识符(除非被重新定义),它的值为 undefined,
通过 void 运算符即可得到该值。

var a = 11

console.log(void a, a) // undefined  11

void 并不改变表达式的结果,只是让表达式不返回值,可以用 void 0 来获得 undefined,void 0 和 void 1, void ...并没有实质上的区别,都是 undefined。

function demo() {
  if (error) {
    console.log(error)
    return void dosomething(error)
  }
}

为了让 if 语句停止向下运行,所以使用 void 表达式,上面例子是等同于:

if (error) {
  dosomething(error)
  return
}
  • 特殊的数字(NaN)(not a number)

通过数学运算符(+、-、x、/)得到的如果不是一个有效数字,就会返回 NaN。

var a = 11 / 'kyrie' // NaN
typeof a // 'number'

可以看到,NaN 也是数字类型。

NaN 是一个“警戒值”,用于指出数字类型的错误情况,即“执行数学运算没有成功”。

  • 检测 NaN
var a = 11 / 'kyrie'

a == NaN // false
a === NaN // false

注意:NaN 是一个特殊值,它不等于自身,唯一一个非自反(自反,x === x 不成立)的值。

所以,我们可以利用 NaN 不等于自身的特性来检测一个变量的值是否为 NaN

function isNaN(n) {
  return n !== n;
}
var a = 11 / 'kyrie'
isNaN(a) // true
  • 无穷数
var a = 11 / 0 // Infinity(正无穷大)
var a = -11 / 0 // -Infinity(负无穷大)

注意:Infinity / Infinity 结果为 NaN, 正整数除以 Infinity 结果为 0

  • 零值

JavaScript 中有一个常规的 0 和一个-0

var a = 0 / -11 // -0
var b = 0 * 11 // 0

加减法不会得到-0

注意:根据规范,对-0 进行字符串化(stringify)会返回'0',对'-0'转换为数字(Number/parse)是 -0

var a = 0 / -11
JSON.stringify(a) // '0'

var b = '-0'
JSON.parse(b) // -0

JSON.stringify(-0)返回 0,JSON.parse('-0')返回-0

  • 如何区分 0 和-0
0 === -0 // true

function isNegZero(n) {
  n = Number(n);
  return (n === 0) && (1 / n === -Infinity);
}

isNegZero(-0) // true
isNegZero(0) // false
  • 为什么需要-0

有些时候,我们需要用数字的符号位(+ / -)来代表某些信息(比如移动方向),如果一个值为 0 的变量没有符号位(即+ / -),那么会丢失方向信息。所以这也是为什么需要-0 的原因。

  • ES6 的工具函数:Object.is()
var a = 11 / 'kyrie';
var b = -11 / 0;

Object.is(a, NaN) // true
Object.is(b, -0) // true
Object.is(b, 0) // false

总结

  1. JavaScript 中的数组是通过数字索引的一组任意类型的值。
  2. null 类型只有一个值 null,undefined 类型也只有一个值 undefined。所有变量在未赋值前默认都是 undefined,void 运算符返回 undefined。
  3. 数字类型有几个特殊值,包括 NaN(not a number)、Ifinity、-Infinity 和-0。
查看原文

赞 0 收藏 0 评论 0

叫我欧文就好 发布了文章 · 4月10日

深入理解JavaScript中的类型

一、类型的定义

大多数人认为,像 JavaScript 这样的动态语言是没有类型(type)的。

也有人认为,JavaScript 中的“类型”应该称为“标签”(tag)或者“子类型”(subtype)。

定义:类型是值的内部特征,它定义了值的行为,目的是为了区分其他值。

怎么理解?变量是没有类型的,值才具有类型,类型描述了值的行为特征;换句话说,当某个值发生改变,类型也随之改变(如 a = 1 --> a = '1')。

二、内置类型(七种)

JavaScript中有七种内置类型:

  • 空值(null)
  • 未定义(undefined)
  • 数字(number)
  • 字符串(string)
  • 对象(object)
  • 布尔值(boolean)
  • 符号(symbol,ES6中新增
除了对象之外,其他统称为‘基本类型’。

三、检测值的类型(typeof)

我们可以使用typeof运算符来查看值的类型,注意:它的返回值是字符串(string)

typeof undefined // 'undefined'
typeof 11 // 'number'
typeof 'kyrie' // 'string'
typeof { name: 'kyrie' } // 'object'
typeof true // 'boolean'=
typeof null // 'object' 远古bug(二进制问题)
// ES6新增类型
typeof Symbol('11') // 'symbol'

你可能注意到少了null类型,而且typeof null 竟然返回的'object'类型,这是个远古bug,存在将近20年,但这个bug应该不会修复了,一修复可能牵扯到的web系统都崩了...

提示:文末有关于这个bug的详细解释。

我们可以使用&&来检测null值的类型

var a = null
if (!a && typeof null === 'object') // true
  • object的子类型(函数)
typeof function demo() {...} // 'object'

函数实际是object的一个“子类型”

函数是“可调用的对象”,他也可以拥有属性

function demo(a, b) {...}
demo.length // 2

函数(对象)的length属性是形参的个数

  • object的子类型(数组)
var arr = [1, 2, 3]
typeof arr // 'object'
arr.length // 3

数组也是object的一个“子类型”

数组(对象)的length属性是元素的个数

四、undefined和undeclared

变量在定义了(但没有值)时为undefined

var a;
typeof a // 'undefined'

那undeclared又是什么?

undeclared(未声明),大多数人认为它和 undefined(未定义值) 是同一个东西,但其实不是。

区别:已经在作用域中声明了但还没有赋值的变量,是undefined的;还没有在作用域中声明过的变量,是undeclared的。
var a;
a // undefined
b // b is not defined (undeclaced)

这里,浏览器的处理很让人误解, “b is not defined”很容易让人误以为 b 是 undefined(未定义值)的,但其实b是undeclaced(未声明)的,如果浏览器报错为:“b is not found” 或者 “b is not declared” 或许更准确一点。

更气人的是,typeof对这类处理让人绝望。

var a;
typeof a //'undefined'
typeof b //'undefined'返回 undeclared会好很多

对于undeclared或者undefined的变量,typeof的返回值都是'undefined';但请注意,这里typeof b没有报错(直接访问b会报错),这是typeof一种特殊安全防范机制

五、typeof的安全防范机制

可以用来检测某个全局变量是否存在

// 报错 debug是未声明(undeclared)的
if (debug) {
  console.log('Debugging is starting!!!')
}

// 安全 typeof的安全防范机制
if (typeof debug !== 'undefined') {
  console.log('Debugging is starting!!!')
}

某些时候也可以不用typeof的安全防范机制

if (window.debug) {
  console.log('Debugging is starting!!!')
}

上面的实现方式只适用于代码运行在浏览器环境(拥有window对象),但其他环境不行(如Node.js/服务端...),因为全局对象并不是window。

总结

  1. JavaScript中有七种内置类型:null、undefined、boolean、number、string、object、symbol,可以使用typeof运算符来判断某个值的类型。
  2. 变量是没有类型的,只有值有类型,类型定义(描述)了值的行为特征。
  3. undefined和undeclared不是同一个东西。undefined是值的类型之一,undeclared表示变量还未声明。
  4. 直接访问undeclared变量是会报错的(ReferenceError: xxx is not defined),而且typeof对undefined和undeclared变量都返回'undefined'。
  5. 可以通过typeof的安全防范机制(阻止报错)来检查undeclared变量
附:关于typeof null === 'object'的解释

在第一版的 JavaScript 中,变量的值被设计保存在一个 32 位的内存单元中。该单元包含一个 1 或 3 位的类型标志,和实际数据的值。类型标志存储在单元的最后。包括以下5种情况:

  • 000:object - 数据为对象的引用
  • 1:int - 数据为31位的有符号整型
  • 010:double - 数据为一个浮点数的引用
  • 100:string - 数据为一个字符串的引用
  • 110:boolean - 数据为布尔类型的值

除此之外,还有两种特殊情况:

  • undefined 负的 2 的 30 次方(超出当时整型取值范围的一个数)
  • null 空指针(全是0)

正是因为:null 的存储单元最后三位和 object 一样是 000(罪魁祸首)

所以typeof null === 'object' // true

查看原文

赞 0 收藏 0 评论 0

叫我欧文就好 发布了文章 · 4月7日

彻底搞懂JavaScript中的作用域和闭包

一、作用域

  • 作用域是什么

几乎所有的编程语言都有一个基本功能,就是能够存储变量的值,并且能在之后对这个值进行访问和修改。

那这些变量存储在哪里?怎么找到它?因为只有找到它才能对它进行访问和修改。

简单来说,作用域就是一套规则,用于确定在何处以及如何查找变量(标识符)。


那么问题来了,究竟在哪里设置这些作用域的规则呢?怎样设置?

首先,我们要知道,一段代码在执行之前会经历三个步骤,统称为“编译”。

  1. 分词/词法分析

这个过程会将字符串分解成有意义的代码块,这些代码块称为词法单元

var a = 1;
// 这段代码会被分解为五个词法单元:
var 、 a 、 = 、 1 、 ;
  1. 解析/语法分析

这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表语法结构的树。这个树称为“抽象语法树(AST)

  1. 代码生成

这个过程是将AST转换为可执行的代码

简单来说,用某种方法可以将
var a = 2; 
的抽象语法树(AST)转化为一组机器指令,
指令用来创建一个叫作a的变量,并将一个值2存在a中

在这个过程中,有3个重要的角色:

  1. 引擎:从头到尾负责整个JavaScript程序的编译及执行过程
  2. 编译器:负责语法分析及代码生成
  3. 作用域(今天的主角):负责收集并维护由所有声明的变量(标识符)注册的一系列查询,并实施一套严格的规则,确定当前执行的代码对这些变量的访问权限。

所以,看似简单的一段代码 var a = 1; 编译器是怎么处理的呢?

var a = 1;
  1. 首先,遇到 var a, 编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域中。如果是,编译器会忽略该声明,继续下一步。否则编译器会要求作用域在当前作用域中声明一个新变量,并命名为a
  2. 其次,编译器会为引擎生成运行时所需的代码,用来处理 a = 1 这个赋值操作。引擎运行时首先询问作用域,当前作用域是否存在一个叫a的变量,如果是,引擎会使用这个变量,否则引擎会继续查找该变量,如果找到了,就会将1赋值给它,否则引擎会抛出一个异常。

那么,引擎是如何查找变量的?

引擎会为变量 a 进行LHS查询(左侧)。另外一个叫RHS查询(右侧)

简单来说,LHS查询就是试图找到变量的容器本身(比如a);而RHS查询就是查询某个变量的值(比如1)

总结:作用域就是根据名称查找变量的一套规则


  • 作用域嵌套

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。

function add(a) {
  console.log(a + b)
}

var b = 2;

add(1) // 3

在add()内部对b进行RHS查询,发现查询不到,但可以在上一级作用域(这里是全局作用域)中查询到。

怎么区分LHS和RHS查询?思考以下代码

function add(a) {
  // 对b进行RHS查询 无法找到(未声明)
  console.log(a + b) // 对变量b来说,取值操作
  b = a // 对变量b来说,赋值操作
}

add(1) // ReferenceError: b is not defined
function add(a) {
  // 对b进行LHS查询,无法找到,会自动创建一个全局变量window.b(非严格模式)
  b = a  // 对变量b来说,赋值操作
  console.log(a + b)// 对变量b来说,取值操作
}

add(1) // 2

总结:如果查找变量的目的是赋值,则进行LHS查询;如果是取值,则进行RHS查询


  • 词法作用域

作用域有两种主要的工作模型。第一种最为普遍,也是重点,叫作词法作用域,另一种叫作动态作用域(几乎不用)

简单来说,词法作用域就是定义在词法阶段的作用域(通俗易懂的说,就是在写代码时变量或者函数声明的位置)。

function foo(a) {
  var b = a * 2
  
  function bar(c) {
    console.log(a, b, c)
  }
  
  bar(b * 3)
}

foo(2) // 2, 4, 12
  1. 全局作用域中有1个变量:foo
  2. foo作用域中有3个变量:a、b、bar
  3. bar作用域中有1个变量:c

变量查找的过程:首先从最内部的作用域(即bar函数)的作用域开始查找,引擎无法找到变量a,因此会到上一级作用域(foo函数)中继续查找,在这里找到了变量a,因此引擎使用了这个引用。变量b同理,对于变量c来说,引擎在bar函数中的作用域就找到了它。

注意:作用域查找会在找到第一个匹配的变量(标识符)时停止查找


  • 函数作用域

简单来说,函数作用域是指,属于这个函数的全部变量都可以在这个函数范围内使用及复用(复用:即在嵌套的其他作用域中也可以使用)。

var a = 1

// 定义一个函数包裹代码块,形成函数作用域
function foo() {
  var a = 2
  console.log(a) // 2
}

foo()
console.log(a) // 1

你会觉得,如果我要使用函数作用域,那么我必须定义一个foo函数,这让全局作用域多了个函数,污染了全局作用域,且必须执行一次该函数才能运行其中的代码块。

那有没有一种办法,可以让我不污染全局作用域(即不定义新的具名函数),且函数可以自动执行呢?

你一定想到了,IIFE(立即执行函数)

var a = 1;
(function foo() {
  var a = 2
  console.log(a) // 2
})()
console.log(a) // 1

这种写法,实际上不是一个函数声明,而是一个函数表达式。要区分这两者,最简单的方法就是看function关键字是否出现在第一个位置(第一个词),如果是,那么是函数声明,否则是一个函数表达式。

  • 块作用域

尽管你可能没写过块作用域的代码,但你一定对下面的代码块很熟悉:

for(var i = 0; i < 5; i++) {
  console.log(i)
}

我们在for循环的头部定义了变量i,是因为想在for循环内部的上下文中使用i,而忽略了最重要的一点:i会被绑定在外部作用域(即全局作用域中)。

ES6改变了这种情况,引入let关键字,提供另一种声明变量的方式。

{
  let a = 2;
  console.log(a) // 2
}
console.log(a) // ReferenceError: a is not defined

讨论一下之前的for循环

for(let i = 0; i < 5; i++) {
  console.log(i)
}
console.log(i) // ReferenceError: i is not defined

这里,for循环头部的i绑定在循环内部,其实它在每一次循环中,对i进行了重新赋值。

{
  let j;
  for(let j = 0; j < 5; j++) {
    let i = j // 每次循环重新赋值
    console.log(i)
  }
  j++
}
console.log(i) // ReferenceError: i is not defined

小知识:其实在ES6之前,使用try/catch结构(在catch分句中)也有块作用域


  • 提升

先有鸡(声明)还是先有蛋(赋值)?

简单来说,一个作用域中,包括变量和函数在内的所有声明都会在任何代码被执行前首先被 “移动” 到作用域的最顶端,这个过程就叫作提升。

a = 2
var a
console.log(a) // 2

// 引擎解析:
var a
a = 2
console.log(a) // 2
console.log(a) // undefined
var a = 2

//引擎解析:
var a
console.log(a) // undefined
a = 2

可以发现,当JavaScript看到 var a = 2; 时,会分成两个阶段,编译阶段执行阶段

编译阶段:定义声明,var a

执行阶段: 赋值声明,a = 2

结论:先有蛋(声明),后有鸡(赋值)。

  • 函数优先

函数和变量都会提升,但函数会首先被提升,然后是变量。

foo() // 2

var foo = 1

function foo() {
  console.log(2)
}

foo = function() {
  console.log(3)
}

// 引擎解析:
function foo() {...}
foo()
foo = function() {...}

多个同名函数,后面的会覆盖前面的函数

foo() // 3

var foo = 1

function foo() {
  console.log(2)
}

function foo() {
  console.log(3)
}

提升不受条件判断控制

foo() // 2

if (true) {
  function foo() {
    console.log(1)
  }
} else {
  function foo() {
    console.log(2)
  }
}

注意:尽量避免普通的var声明和函数声明混合在一起使用。

二、闭包

  • 定义:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

秘诀:JavaScript中闭包无处不在,你只需要能够识别并拥抱它。

function foo() {
  var a = 2
  
  function bar() {
    console.log(a)
  }
  
  return bar
}

var baz = foo()
baz() // 2 快看啊,这就是闭包!!!

函数bar()的词法作用域能够访问foo()的内部作用域,然后将bar()本身当作一个值类型进行传递。

正常情况下,当foo()执行后,foo()内部的作用域都会被销毁(引擎的垃圾回收机制),而闭包的“神奇”之处就是可以阻止这件事请的发生。事实上foo()内部的作用域依然存在,不然bar()里面无法访问到foo()作用域内的变量a

foo()执行后,bar()依然持有该作用域的引用,而这个引用就叫作闭包

总结:无论何时何地,如果将函数当作值类型进行传递,你就会看到闭包在这些函数中的应用(定时器,ajax请求,事件监听器...)。

我相信你懂了!

回顾一下之前提到的for循环

for(var i = 0; i < 10; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

期望:每秒依次打印1、2、3、4、5...9

结果:每秒打印的都是10

稍稍改进一下代码(利用IIFE)

for(var i = 0; i < 10; i++) {
  (function(i) {
    setTimeout(function timer() {
        console.log(i)
    }, i * 1000)
  })(i)
}

问题解决!对了,我们差点忘了let关键字

for(var i = 0; i < 10; i++) {
  let j = i // 闭包的块作用域
  setTimeout(function timer() {
    console.log(j)
  }, j * 1000)
}

还记得吗?之前有提到,for循环头部的let声明在每次迭代都会重新声明赋值,而且每个迭代都会使用上一个迭代结束的值来进行这次值的初始化。

最终版:

for(let i = 0; i < 10; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

好了,现在你肯定懂了!

总结:当函数可以记住并访问所在的词法作用域,即使函数是在当前的词法作用域之外执行,就产生了闭包

如果你坚持看到了这里,我替你感到高兴,因为你已经掌握了JavaScript中的作用域和闭包,这些知识都是进阶必备的,如果有不理解的,花时间多看几遍,相信你一定可以掌握其中的精髓。

都到这儿了!

点个关注再走呗!!

查看原文

赞 9 收藏 6 评论 0

叫我欧文就好 发布了文章 · 4月6日

彻底搞懂JavaScript中的this关键字

本文对JavaScript中的this关键字进行全方位的解析,看完本篇文章,希望读者们能够完全理解this的绑定问题。

开篇:对于那些没有投入时间去学习this机制的JavaScript开发者来说,this的绑定是一件令人困惑的事。(包括曾经的自己)。

误区:学习this的第一步是明白this既不指向函数本身也不指向函数的词法作用域,你是否被类似这样的解释所误导?但其实这种说法都是错误的。

概括:this实际是在函数被调用时发生的绑定,它所指向的位置完全取决于函数被调用的位置。

一、调用位置

在理解this的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)。

所以说,寻找调用位置就是寻找“函数被调用的位置”,这里最重要的点是要分析调用栈(存放当前正在执行的函数的位置)

什么是调用栈和调用位置?

关系:调用位置就在当前正在执行的函数(调用栈)的前一个位置

function func1() {
  // 当前调用栈:func1
  // 当前调用位置是全局作用域(调用栈的前一个位置)
  console.log('func1')
  func2() // 这里是:func2的调用位置
}
function func2() {
  // 当前调用栈:func1 -> func2
  // 当前调用位置是在func1(调用栈的前一个位置)
  console.log('func2')
  func3() // 这里是:func3的调用位置
}
function func3() {
  // 当前调用栈:func1 -> func2 -> func3
  // 当前调用位置是在func2(调用栈的前一个位置)
  console.log('func3')
}
func1() // 这里是:func1的调用位置

关注点:我们是如何从调用栈中分析出真正的调用位置的,因为这决定了this的绑定

二、绑定规则

  • 默认绑定

最常用的函数调用类型:独立函数调用

function getName() {
  console.log(this.name)
}
var name = 'kyrie'
getName() // 'kyrie'

当调用getName()时,this.name拿到了全局对象的name。因为getName()是直接调用的,不带任何修饰符,使用的是默认绑定,因此this指向全局对象(非严格模式)。

如果使用严格模式('strict mode')呢?

function getName() {
  'use strict';
  console.log(this.name)
}
var name = 'kyrie'
getName() // 'TypeError: this is undefined'

那么全局对象无法使用默认绑定,因此this会绑定到undefined

  • 隐式绑定

调用位置是否有上下文对象

function getName() {
  console.log(this.name)
}
var person = {
  name: 'kyrie',
  getName: getName
}
person.getName() // 'kyrie'

当getName()被调用时,它的落脚点指向person对象,当函数引用有上下文对象时,隐式绑定会把函数调用中的this绑定到这个上下文对象,因此调用getName()时this被绑定到person,因此this.name跟person.name是一样的

常见问题:隐式丢失?

function getName() {
  console.log(this.name)
}
var person = {
  name: 'kyrie',
  getName: getName
}
var getName2 = person.getName() // 函数别名
var name = 'wen' // name是全局对象的属性
getName2() // 'wen' 这里拿到的是全局对象的name

解释:虽然getName2是person.getName的一个函数引用,但它引用的getName函数的本身,因此getName2()调用时不带任何修饰符,使用的是默认绑定,因此this绑定了全局对象

  • 显式绑定

使用call() / apply() / bind() 指定this的绑定对象

function getName() {
  console.log(this.name)
}
var person = {
  name: 'kyrie'
}
getName.call(person) // 'kyrie'
getName.apply(person) // 'kyrie'

通过getName.call()/ getName.apply() 调用强制把它的this绑定到person上。

  • new绑定

所有函数都可以用new来调用,这种函数调用称为构造函数调用

重点:实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”

使用new来调用函数,或者说发生构造函数调用时,会自动执行以下的四步操作:
  1. 创建(或者构造)一个新的对象
  2. 这个新对象会被执行[[原型]]连接(暂时忽略,属于原型内容,后面再介绍它)
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,则new表达式中的函数会自动返回这个新的对象
function setName(name) {
  this.name = name
}
var person = new setName('kyrie')
console.log(person.name) // 'kyrie'

使用new调用setName()时,会创建一个新对象并把这个新对象绑定到setName()调用的this上,并把这个对象返回

三、优先级

毫无疑问,默认绑定的优先级是四条规则中最低的,所以暂不考虑它。

  1. 隐式绑定和显式绑定哪个优先级高?
function getName() {
  console.log(this.name)
}
var p1 = {
  name: 'kyrie',
  getName: getName
}
var p2 = {
  name: 'wen',
  getName: getName
}
p1.getName() // 'kyrie'
p2.getName() // 'wen'
p1.getName.call(p2) // 'wen'
p2.getName.call(p1) // 'kyrie'

结果,显式绑定的优先级比隐式绑定高。

  1. 隐式绑定和new绑定哪个优先级高?
function setName(name) {
  this.name = name
}
var p1 = {
  setName: setName
}
var p2 = {}
p1.setName('kyrie')
console.log(p1.name) // 'kyrie'
p1.setName.call(p2, 'wen')
console.log(p2.name) // 'wen'
var p3 = new p1.setName('zbw')
console.log(p1.name) // 'kyrie'
console.log(p3.name) // 'zbw'

结果,new绑定的优先级比隐式绑定高

  1. 显式绑定和new绑定的哪个优先级高?
function setName(name) {
  this.name = name
}
var p1 = {}
// bind会返回一个新的函数
var setP1Name = setName.bind(p1)
setP1Name('kyrie')
console.log(p1.name) // 'kyrie'
var p2 = new setP1Name('wen')
console.log(p1.name) // 'kyrie'
console.log(p2.name) // 'wen'

结果,new绑定的优先级比显示绑定高

综上,优先级的正确排序:

从高到低: new > 显示 > 隐式 > 默认

  • 判断this的指向

现在我们可以根据优先级来判断函数在某个位置调用this的指向。

  1. 函数是否通过new来调用(new绑定)?如果是,则this指向新创建的对象
var p1 = new Person()
  1. 函数是否通过call/apply/bind调用(显式绑定)?如果是,则this指向第一个参数
var p1 = setName.call(p2)
  1. 函数是否在某个上下文对象中调用(隐式绑定)?如果是,则this指向该上下文对象
var p2 = p1.setName()
  1. 如果以上三个条件都不满足,则使用默认绑定。如果是在严格模式中,this指向undefined,否则指向全局对象。
var p1 = setName()

四、箭头函数的this

以上上提到判断this指向的四条规则包含所有正常的函数,除了ES6中的箭头函数

概括:箭头函数不像普通函数那样使用function关键字定义,而是用 “胖箭头” => 定义 。而且箭头函数并不适用以上的四条规则,它的this绑定完全是根据 外层作用域(函数或者全局) 来决定的。

function getName() {
  // 箭头函数的this指向外层作用域
  return (name) => {
    console.log(this.name)
  }
}
var p1 = {
  name: 'kyrie'
}
var p2 = {
  name: 'wen'
}
var func = getName.call(p1)
func.call(p2) // 'kyrie'

getName()内部创建的箭头函数会捕获调用时外层作用域(getName)的this,由于getName的this通过显示绑定到p1上,所以getName里创建的箭头函数也会指向p1,最重要的一点:箭头函数的this无法被修改(即使是优先级最高的new绑定也不行

总结

要判断一个运行中的函数的this绑定,需要找到该函数的调用位置(结合调用栈),接着根据优先级得出的四条规则来判断this的绑定对象。

  1. 函数由new调用?绑定到新创建的对象
  2. 由call/apply/bind调用?绑定到指定对象
  3. 由上下文对象调用?绑定到上下文对象
  4. 默认:严格模式下绑定到undefined,否则绑定到全局对象

ES6的箭头函数不适用以上四条规则,而是根据当前的词法作用域来决定this绑定,也就是说,箭头函数会继承外层函数调用的this绑定(无论绑定到什么),而且箭头函数的this绑定无法被修改

查看原文

赞 1 收藏 1 评论 0

叫我欧文就好 赞了回答 · 4月5日

解决怎么理解for循环中用let声明的迭代变量每次是新的变量?

按你的理解,还有点不对,虽然跨块,但你变的仍然是 i 本身,我认为应该是这样(仍然按你的思路)

{
    let i = 0;
    {
        let _i = i;
        a[_i] = function() {
            console.log(_i);
        };
    }
    i++;
}

再来给你看个神奇的示例,可以证明你的理解基本正确(虽然给的代码错了,但是我理解了你的意思)

for (var i = 0; i < 3; console.log("in for expression", i), i++) {
    let i;
    console.log("in for block", i);
}

clipboard.png

关注 17 回答 7

叫我欧文就好 赞了文章 · 4月3日

30分钟,让你彻底明白Promise原理

原文链接

前言

前一阵子记录了promise的一些常规用法,这篇文章再深入一个层次,来分析分析promise的这种规则机制是如何实现的。ps:本文适合已经对promise的用法有所了解的人阅读,如果对其用法还不是太了解,可以移步我的上一篇博文

本文的promise源码是按照Promise/A+规范来编写的(不想看英文版的移步Promise/A+规范中文翻译

引子

为了让大家更容易理解,我们从一个场景开始讲解,让大家一步一步跟着思路思考,相信你一定会更容易看懂。

考虑下面一种获取用户id的请求处理

//例1
function getUserId() {
    return new Promise(function(resolve) {
        //异步请求
        http.get(url, function(results) {
            resolve(results.id)
        })
    })
}

getUserId().then(function(id) {
    //一些处理
})

getUserId方法返回一个promise,可以通过它的then方法注册(注意注册这个词)在promise异步操作成功时执行的回调。这种执行方式,使得异步调用变得十分顺手。

原理剖析

那么类似这种功能的Promise怎么实现呢?其实按照上面一句话,实现一个最基础的雏形还是很easy的。

极简promise雏形

function Promise(fn) {
    var value = null,
        callbacks = [];  //callbacks为数组,因为可能同时有很多个回调

    this.then = function (onFulfilled) {
        callbacks.push(onFulfilled);
    };

    function resolve(value) {
        callbacks.forEach(function (callback) {
            callback(value);
        });
    }

    fn(resolve);
}

上述代码很简单,大致的逻辑是这样的:

  1. 调用then方法,将想要在Promise异步操作成功时执行的回调放入callbacks队列,其实也就是注册回调函数,可以向观察者模式方向思考;
  2. 创建Promise实例时传入的函数会被赋予一个函数类型的参数,即resolve,它接收一个参数value,代表异步操作返回的结果,当一步操作执行成功后,用户会调用resolve方法,这时候其实真正执行的操作是将callbacks队列中的回调一一执行;

可以结合例1中的代码来看,首先new Promise时,传给promise的函数发送异步请求,接着调用promise对象的then属性,注册请求成功的回调函数,然后当异步请求发送成功时,调用resolve(results.id)方法, 该方法执行then方法注册的回调数组。

相信仔细的人应该可以看出来,then方法应该能够链式调用,但是上面的最基础简单的版本显然无法支持链式调用。想让then方法支持链式调用,其实也是很简单的:

this.then = function (onFulfilled) {
    callbacks.push(onFulfilled);
    return this;
};

see?只要简单一句话就可以实现类似下面的链式调用:

// 例2
getUserId().then(function (id) {
    // 一些处理
}).then(function (id) {
    // 一些处理
});

加入延时机制

细心的同学应该发现,上述代码可能还存在一个问题:如果在then方法注册回调之前,resolve函数就执行了,怎么办?比如promise内部的函数是同步函数:

// 例3
function getUserId() {
    return new Promise(function (resolve) {
        resolve(9876);
    });
}
getUserId().then(function (id) {
    // 一些处理
});

这显然是不允许的,Promises/A+规范明确要求回调需要通过异步方式执行,用以保证一致可靠的执行顺序。因此我们要加入一些处理,保证在resolve执行之前,then方法已经注册完所有的回调。我们可以这样改造下resolve函数:

function resolve(value) {
    setTimeout(function() {
        callbacks.forEach(function (callback) {
            callback(value);
        });
    }, 0)
} 

上述代码的思路也很简单,就是通过setTimeout机制,将resolve中执行回调的逻辑放置到JS任务队列末尾,以保证在resolve执行时,then方法的回调函数已经注册完成.

但是,这样好像还存在一个问题,可以细想一下:如果Promise异步操作已经成功,这时,在异步操作成功之前注册的回调都会执行,但是在Promise异步操作成功这之后调用的then注册的回调就再也不会执行了,这显然不是我们想要的。

加入状态

恩,为了解决上一节抛出的问题,我们必须加入状态机制,也就是大家熟知的pendingfulfilledrejected

Promises/A+规范中的2.1Promise States中明确规定了,pending可以转化为fulfilledrejected并且只能转化一次,也就是说如果pending转化到fulfilled状态,那么就不能再转化到rejected。并且fulfilledrejected状态只能由pending转化而来,两者之间不能互相转换。一图胜千言:

alt promise state

改进后的代码是这样的:

function Promise(fn) {
    var state = 'pending',
        value = null,
        callbacks = [];

    this.then = function (onFulfilled) {
        if (state === 'pending') {
            callbacks.push(onFulfilled);
            return this;
        }
        onFulfilled(value);
        return this;
    };

    function resolve(newValue) {
        value = newValue;
        state = 'fulfilled';
        setTimeout(function () {
            callbacks.forEach(function (callback) {
                callback(value);
            });
        }, 0);
    }

    fn(resolve);
}

上述代码的思路是这样的:resolve执行时,会将状态设置为fulfilled,在此之后调用then添加的新回调,都会立即执行。

这里没有任何地方将state设为rejected,为了让大家聚焦在核心代码上,这个问题后面会有一小节专门加入。

链式Promise

那么这里问题又来了,如果用户再then函数里面注册的仍然是一个Promise,该如何解决?比如下面的例4

// 例4
getUserId()
    .then(getUserJobById)
    .then(function (job) {
        // 对job的处理
    });

function getUserJobById(id) {
    return new Promise(function (resolve) {
        http.get(baseUrl + id, function(job) {
            resolve(job);
        });
    });
}

这种场景相信用过promise的人都知道会有很多,那么类似这种就是所谓的链式Promise

链式Promise是指在当前promise达到fulfilled状态后,即开始进行下一个promise(后邻promise)。那么我们如何衔接当前promise和后邻promise呢?(这是这里的难点)。

其实也不是辣么难,只要在then方法里面return一个promise就好啦。Promises/A+规范中的2.2.7就是这么说哒(微笑脸)~

下面来看看这段暗藏玄机的then方法和resolve方法改造代码:


function Promise(fn) {
    var state = 'pending',
        value = null,
        callbacks = [];

    this.then = function (onFulfilled) {
        return new Promise(function (resolve) {
            handle({
                onFulfilled: onFulfilled || null,
                resolve: resolve
            });
        });
    };

    function handle(callback) {
        if (state === 'pending') {
            callbacks.push(callback);
            return;
        }
        //如果then中没有传递任何东西
        if(!callback.onFulfilled) {
            callback.resolve(value);
            return;
        }

        var ret = callback.onFulfilled(value);
        callback.resolve(ret);
    }

    
    function resolve(newValue) {
        if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
            var then = newValue.then;
            if (typeof then === 'function') {
                then.call(newValue, resolve);
                return;
            }
        }
        state = 'fulfilled';
        value = newValue;
        setTimeout(function () {
            callbacks.forEach(function (callback) {
                handle(callback);
            });
        }, 0);
    }

    fn(resolve);
}

我们结合例4的代码,分析下上面的代码逻辑,为了方便阅读,我把例4的代码贴在这里:

// 例4
getUserId()
    .then(getUserJobById)
    .then(function (job) {
        // 对job的处理
    });

function getUserJobById(id) {
    return new Promise(function (resolve) {
        http.get(baseUrl + id, function(job) {
            resolve(job);
        });
    });
}
  1. then方法中,创建并返回了新的Promise实例,这是串行Promise的基础,并且支持链式调用。
  2. handle方法是promise内部的方法。then方法传入的形参onFulfilled以及创建新Promise实例时传入的resolve均被push到当前promisecallbacks队列中,这是衔接当前promise和后邻promise的关键所在(这里一定要好好的分析下handle的作用)。
  3. getUserId生成的promise(简称getUserId promise)异步操作成功,执行其内部方法resolve,传入的参数正是异步操作的结果id
  4. 调用handle方法处理callbacks队列中的回调:getUserJobById方法,生成新的promisegetUserJobById promise
  5. 执行之前由getUserId promisethen方法生成的新promise(称为bridge promise)的resolve方法,传入参数为getUserJobById promise。这种情况下,会将该resolve方法传入getUserJobById promisethen方法中,并直接返回。
  6. getUserJobById promise异步操作成功时,执行其callbacks中的回调:getUserId bridge promise中的resolve方法
  7. 最后执行getUserId bridge promise的后邻promisecallbacks中的回调。

更直白的可以看下面的图,一图胜千言(都是根据自己的理解画出来的,如有不对欢迎指正):

alt promise analysis

失败处理

在异步操作失败时,标记其状态为rejected,并执行注册的失败回调:

//例5
function getUserId() {
    return new Promise(function(resolve) {
        //异步请求
        http.get(url, function(error, results) {
            if (error) {
                reject(error);
            }
            resolve(results.id)
        })
    })
}

getUserId().then(function(id) {
    //一些处理
}, function(error) {
    console.log(error)
})

有了之前处理fulfilled状态的经验,支持错误处理变得很容易,只需要在注册回调、处理状态变更上都要加入新的逻辑:

function Promise(fn) {
    var state = 'pending',
        value = null,
        callbacks = [];

    this.then = function (onFulfilled, onRejected) {
        return new Promise(function (resolve, reject) {
            handle({
                onFulfilled: onFulfilled || null,
                onRejected: onRejected || null,
                resolve: resolve,
                reject: reject
            });
        });
    };

    function handle(callback) {
        if (state === 'pending') {
            callbacks.push(callback);
            return;
        }

        var cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected,
            ret;
        if (cb === null) {
            cb = state === 'fulfilled' ? callback.resolve : callback.reject;
            cb(value);
            return;
        }
        ret = cb(value);
        callback.resolve(ret);
    }

    function resolve(newValue) {
        if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
            var then = newValue.then;
            if (typeof then === 'function') {
                then.call(newValue, resolve, reject);
                return;
            }
        }
        state = 'fulfilled';
        value = newValue;
        execute();
    }

    function reject(reason) {
        state = 'rejected';
        value = reason;
        execute();
    }

    function execute() {
        setTimeout(function () {
            callbacks.forEach(function (callback) {
                handle(callback);
            });
        }, 0);
    }

    fn(resolve, reject);
}

上述代码增加了新的reject方法,供异步操作失败时调用,同时抽出了resolvereject共用的部分,形成execute方法。

错误冒泡是上述代码已经支持,且非常实用的一个特性。在handle中发现没有指定异步操作失败的回调时,会直接将bridge promise(then函数返回的promise,后同)设为rejected状态,如此达成执行后续失败回调的效果。这有利于简化串行Promise的失败处理成本,因为一组异步操作往往会对应一个实际功能,失败处理方法通常是一致的:

//例6
getUserId()
    .then(getUserJobById)
    .then(function (job) {
        // 处理job
    }, function (error) {
        // getUserId或者getUerJobById时出现的错误
        console.log(error);
    });

异常处理

细心的同学会想到:如果在执行成功回调、失败回调时代码出错怎么办?对于这类异常,可以使用try-catch捕获错误,并将bridge promise设为rejected状态。handle方法改造如下:

function handle(callback) {
    if (state === 'pending') {
        callbacks.push(callback);
        return;
    }

    var cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected,
        ret;
    if (cb === null) {
        cb = state === 'fulfilled' ? callback.resolve : callback.reject;
        cb(value);
        return;
    }
    try {
        ret = cb(value);
        callback.resolve(ret);
    } catch (e) {
        callback.reject(e);
    } 
}

如果在异步操作中,多次执行resolve或者reject会重复处理后续回调,可以通过内置一个标志位解决。

总结

刚开始看promise源码的时候总不能很好的理解then和resolve函数的运行机理,但是如果你静下心来,反过来根据执行promise时的逻辑来推演,就不难理解了。这里一定要注意的点是:promise里面的then函数仅仅是注册了后续需要执行的代码,真正的执行是在resolve方法里面执行的,理清了这层,再来分析源码会省力的多。

现在回顾下Promise的实现过程,其主要使用了设计模式中的观察者模式:

  1. 通过Promise.prototype.then和Promise.prototype.catch方法将观察者方法注册到被观察者Promise对象中,同时返回一个新的Promise对象,以便可以链式调用。
  2. 被观察者管理内部pending、fulfilled和rejected的状态转变,同时通过构造函数中传递的resolve和reject方法以主动触发状态转变和通知观察者。

参考文献

深入理解 Promise
JavaScript Promises ... In Wicked Detail

查看原文

赞 185 收藏 250 评论 38

叫我欧文就好 发布了文章 · 4月2日

20个常用的CSS知识点

1. 如何隐藏滚动条

// chrome 和Safari
*::-webkit-scrollbar { width: 0 !important }
// IE 10+
* { -ms-overflow-style: none; }
// Firefox
* { overflow: -moz-scrollbars-none; }

2. 修改滚动条样式

*::-webkit-scrollbar {
  /*定义纵向滚动条宽度*/
  width: 12px!important;
  /*定义横向滚动条高度*/
  height: 12px!important; 
}
*::-webkit-scrollbar-thumb {
  /*滚动条内部滑块*/
  border-radius: 16px;
  background-color:#c1c1c1;
  transition: background-color 0.3s;
  &:hover {
    /*鼠标悬停滚动条内部滑块*/
    background: #bbb;
  }
 }
*::-webkit-scrollbar-track {
  /*滚动条内部轨道*/
  background: #f1f1f1;
}

3. 修改input框placeholder的颜色

input::input-placeholder{
    color:red;
}

4. 按钮不可点击的样式

cursor: not-allowed

5. CSS鼠标指针事件:阻止任何JS事件

.disabled { pointer-events: none; }

6. 文字超出强制n行 超出部分用省略号代替

div {
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: n; // 行数
  -webkit-box-orient: vertical;
}

7. 修改字体间距

letter-spacing: 8px

8. 谷歌浏览器控制台提示/deep/将要被移除

<style scoped lang="less">
// 采用的less的转义和变量插值
@deep: ~'>>>';
.select {
     @{deep} .ivu-card-body {
        width: 100%;
      }
    }
</style>

9. animate动画停在某个关键帧

animation-fill-mode: forwards;

10. 盒子阴影

box-shadow: 0 2px 2px rgba(10,16,20,.24),0 0 2px rgba(10,16,20,.12);
transition: box-shadow .5s;

11.使图片覆盖它的整个容器

img {
  object-fit: cover;
}

12. 表格中td的内容自动换行

<table style="word-break:break-all; word-wrap:break-all;">

13. 浏览器打印功能 图片失效

body {
    -webkit-print-color-adjust: exact;
}

14. 背景图像完美适配视口

body {
  background-image: url('xxx');
  background-repeat: no-repeat;
  background-position: center;
  background-attachment: fixed;
  background-size: cover;
  -webkit-background-size: cover;
  -moz-background-size: cover;
  -o-background-size: cover;
}

15. 如何使用多个背景图片

body {
  background-image: url('xxx'), url('xxx');
  background-position: center, top;
  background-repeat: repeat, no-repeat;
  background-size: contain, cover;
}

16. 如何给背景图叠加渐变

body {
  background-image: 
    linear-gradient(
      4deg, 
      rgba(38,8,31,0.75) 30%, 
      rgba(213,49,127,0.3) 45%, 
      rgba(232,120,12,0.3) 100%),
      url("xxx");
  background-size: cover;
  background-repeat: no-repeat;
  background-attachment: fixed;
  background-position: center
}

17. 如何将背景图设为文本颜色

<body>
    <h1>hello!!!</h1>
</body>

body {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  width: 100%;
  text-align: center;
  min-height: 100vh;
  font-size: 120px;
}

h1 {
   background-image: url("xxx");
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
}

18. 如何获取和设置盒子的宽高

//第一种
dom.style.width/height //只能获取内联样式的元素宽高
//第二种
dom.currentStyle.width/height //只有IE浏览器支持
//第三种
dom.getComputedStyle(Dom).width/height //只有浏览器渲染后才能获取 兼容好
//第四种
dom.getBoundingClientRect().width/height //计算一个元素的绝对位置(相对于视窗左上角) 能拿到元素的left、right、width、height

19. 如何让图片垂直居中

img {
  vertical-align: middle;
  margin-top: -xpx;
}

20. 消除图片自带的间距

img {
  display: block;
}

// 或者 父盒子

div {
  font-size: 0;
}
查看原文

赞 34 收藏 25 评论 2

叫我欧文就好 发布了文章 · 3月30日

你一定踩过的微信H5的坑

1. IOS的input光标高度问题

问题:IOS的input框高度会默认占满父盒子的高度,导致光标也会撑满整个input框
解决:父盒子使用上下padding撑开,不使用行高(line-height)居中

2. IOS微信h5页面上下滑动时卡顿,页面缺失

问题:如果页面高度超出了一屏,滑动页面就会出现卡顿,有时会有页面显示不全的情况
分析:苹果微信浏览器内核使用自带的safari,需要overflow-scrolling开启回弹效果
解决:*{ -webkit-overflow-scrolling: touch }
注意:如果页面中有绝对定位的节点,该节点的显示会错乱

3.IOS键盘唤起,键盘收起以后页面不归位

问题: 输入内容时,键盘弹出,页面内容整体上移,但是键盘收起,页面内容无法归位
分析:input聚焦时,弹出的软键盘占位,失去焦点时软键盘消失,但还是占位的,导致input框不能再次输入
解决: 监听input的blur方法,失去焦点时,用js滚动页面

4. Android弹出的软键盘遮盖文本框

问题:安卓微信h5页面太长时弹出软键盘后会挡住input输入框
解决:给input和textarea标签添加focus事件,聚焦时延时滚动

5. IOS去除input默认阴影失效

问题:一般去除input阴影我们可以使用outline: none; 也可以使用border: 0;但是我们有时候需要border的情况下,只能使用outline: none; 安卓可以生效,IOS发现阴影还在?
解决:使用-webkit-appearance: none; 即可去除input默认阴影
查看原文

赞 6 收藏 6 评论 0

叫我欧文就好 发布了文章 · 3月23日

JS常用的15个操作数组的方法

JS中那些常用的数组方法:

数组的增删改查:

**push(插入的元素<可传多个>)
作用:往数组的尾部插入元素**

let arr = [1, 2, 3]
arr.push(4, 5)
let res = arr.push(4, 5) //返回值为插入元素后数组的长度(res:5)
console.log(arr) // [1, 2, 3, 4, 5]

**unshift(插入的元素<可传多个>)
作用:往数组的头部插入元素**

let arr = [1, 2, 3]
arr.unshift(-2, -1, 0)
let res = arr.unshift(-2, -1, 0) //返回值为插入元素后数组的长度(res:6)
console.log(arr) // [-2, -1, 0, 1, 2, 3]

**pop()
作用:删除数组的最后一个元素**

let arr = [1, 2, 3]
arr.pop()
let res = arr.pop() // 返回值为删除的元素(res: [3])
console.log(arr) // [1, 2]

**shift()
作用:删除数组的第一个元素**

let arr = [1, 2, 3]
arr.shift()
let res = arr.shift() // 返回值为删除的元素(res: [1])
console.log(arr) // [2, 3]

**slice(开始元素的下标,结束元素的下标)
作用:截取数组特定元素(不改变原数组,可用作浅拷贝)**

// 浅拷贝
let arr = [1, 2, 3]
let newArr = arr.slice() // 返回值为截取后的新数组
console.log(newArr) // [1, 2, 3] 

//截取元素
let arr = [1, 2, 3]
let newArr = arr.slice(0, 2) // 从第下标为0的元素开始截取,到元素下标为2的元素(注:但不包括元素下标为2的元素)
console.log(newArr) // [1, 2]
console.log(arr) // [1, 2, 3] // 不改变原数组

**splice(开始元素的下标,删除的元素个数,添加的元素...)
作用:可新增元素或者删除元素**

// 新增元素
let arr = [1, 3, 5]
arr.splice(1, 0, 2) // 在元素下标为1的元素后面新增一个元素2
let res = arr.splice(1, 0, 2) // 由于此处为新增元素,返回值为空数组[](res: []) 
console.log(arr) // [1, 2, 3, 5]

//删除元素
let arr = [1, 2, 3]
arr.splice(1, 1) // 在元素下标为1的元素后面删除一个元素
let res = arr.splice(1, 1) // 返回值为删除的元素(res: [2])
console.log(arr) // [1, 3]

**join(传入分割每个元素的标志)
作用:数组转换为字符串**

let arr = ['zbw', 'cyl']
let res = arr.join(',') 
console.log(res) // 'zbw, cyl'

let res = arr.join('-')
console.log(res) // 'zbw-cyl'

**concat()
作用:连接多个数组或值**

let arr1 = [1, 2, 3]
let arr2 = [4, 5, 6]

let res = arr1.concat(arr2)
console.log(res) // [1, 2, 3, 4, 5, 6]

let res = arr1.concat(4, arr2, 7, 8)
console.log(res) // [1, 2, 3, 4, 4, 5, 6, 7, 8]

操作数组的高阶函数

高阶函数:把函数作为参数传入或把函数作为返回值的函数为高阶函数

**map()
作用:操作数组,返回经过处理后的数组**

let arr = [1, 2, 3, 4]
let res = arr.map(item => item * 2)
console.log(res) // [2, 4, 6, 8]

**filter()
作用:过滤数组,返回操作表达式为true的数组元素**

let arr = [1, 2, 3, 4]
let res = arr.filter(item => item >= 2)
console.log(res) // [2, 3, 4]

**reduce(pre<初始值>, cur<当前项>, arr<数组本身>, <初始值>)
作用:求和,数组转对象**

// 求和
let arr = [1, 2, 3, 4]
let res = arr.reduce((pre, cur) => {
  return pre += cur
}, 0) // 这里是求和 所以初始值为0
console.log(res) // 10

// 数组转对象
let arr = [
  { id: 1, name: 'zbw', age: 18 },
  { id: 2, name: 'cyl', age: 18 },
]

let obj = arr.reduce((pre, cur) => {
   pre[cur.id] = cur.name
   return pre
}, {}) // 这里是数组转对象 所以初始值为空对象{}

console.log(obj) // {1: 'zbw', 2: 'cyl'} 

**forEach()
作用:跟map()作用类似,区别在于forEach()会改变原数组,且没有返回值**

let arr = [
  { name: 'zbw', age: 22 },
  { name: 'cyl', age: 18 },
]

arr.forEach(item => item.age = 20)
console.log(arr) // [{ name: 'zbw', age: 20 },{ name: 'cyl', age:20 }]

**some()
作用:判断数组中是否存在符合条件表达式的条件,如果有一项符合返回true,否则返回false**

let arr = [1, 2, 3, 4]
let bool = arr.some(item => item > 2)
console.log(bool) // true 因为存在大于2的元素

let bool = arr.some(item => item > 5)
console.log(bool) // false 因为不存在大于5的元素

**every()
作用:判断数组中是否全部元素都符合表达式的条件,全部都符合返回true,否则返回false**

let arr = [1, 2, 3, 4]
let bool = arr.every(item => item > 0)
console.log(bool) // true 因为所有元素都大于0

let bool = arr.every(item => item > 1)
console.log(bool) // false 因为存在不大于1的元素

**find()
作用:找到数组中符合条件的第一个元素**

let arr = [{ id: 1, name: 'zbw', age: 18 }, { id: 2, name: 'cyl', age: 18 }]

let res = arr.find(item => item.age === 18)
console.log(res) // { id: 1, name: 'zbw', age: 18 }

let res = arr.find(item => item.id === 2)
console.log(res) // { id: 2, name: 'cyl', age: 18 }
查看原文

赞 19 收藏 13 评论 2

叫我欧文就好 发布了文章 · 3月22日

useEffect进阶指南(下)

React的内部更新机制?

`<h1 class="name">我的名字是:Kyrie</h1>
<h1 class="name">我的名字是:IRVING</h1>
`

React也跟vue有着Diff算法,它每次render只会更新与上一次渲染不同的节点和内容,所以上面的例子,React只需要这样做:

h1.innerText = Kyrie -> h1.innerText = IRVING

那useEffect呢?

`function Demo() {
  const initCount = 0
  const [count, setCount] = useState(initCount)
  
  // 当我们把count作为依赖传入时,count改变就会重新执行
  useEffect(() => {
    console.log('count的值为:', count)
  }, [count])
  
  return (
    <div>
      <h2>{count}</h2>
      <button onClick={() => setCount(count + 1)}>count++</button>
    </div>
  ) 
}
`

所以useEffect的更新机制完成依靠我们传入的依赖,只要在useEffect里使用到的状态值都必须在依赖中声明,让React内部进行依赖更新。

所以,当依赖的状态变得多起来的时候,难免会让我们在性能方面有所担心。

useEffect传入依赖的正确方式?

现在有个需求:书写一个函数,每秒自动让count+1

`function Demo() {
  const initCount = 0
  const [count, setCount] = useState(initCount)
  
  useEffect(() => {
   // 设定时器,每秒执行一次setCount
    const timer = setInterval(() => {
      setCount(count + 1)
    }, 1000)
    
    return () => {
      clearInterval(timer)
    }
  }, [])
  
  return (
    <div>
      <h2>{count}</h2>
      <button onClick={() => setCount(count + 1)}>count++</button>
    </div>
  ) 
}
`

上面的例子,我们把[]作为useEffect的依赖,就是说effect只会在组件挂载后执行一次。定时器只需要设定一个,每秒setInterval后帮我们把count + 1对吧,这不就能实现需求了吗

看起来貌似没什么问题,但运行起来发现count只自增了一次就停住了,也就是useEffect只执行了一次,what???

原因很简单,我们把[]作为依赖,React内部就会认为effect函数内没有依赖任何值,所以当useEffect第一次执行时,setCount(0 + 1) 此时count = 1,然后1s后,setCount(1 + 1),count = 2? 不,React内部会自动忽略第二次及以后的每次更新,因为我们把[]作为依赖,就代表了effect只会执行一次,第二次开始,count一直都是1,这就导致了count变为1后就不增加了。

那好,我们把count传入作为依赖

`useEffect(() => {
   // 设定时器,每秒执行一次setCount
    const timer = setInterval(() => {
      setCount(count + 1)
    }, 1000)
    
    return () => {
      clearInterval(timer)
    }
  }, [count])
`

运行之后发现,问题确实解决了,count成功的每秒自动自增,但这其实不是最好的解决方案,因为我们知道每当count值改变,就会触发render,每次都会生成一个新的useEffect,然后执行,重新生成一个定时器,虽然目的达到了,但这显然不是最优解,我们最好能避免每次都生成一个新的定时器,因为这样我们的定时器将毫无意义...

`useEffect(() => {
   // 设定时器,每秒执行一次setCount
    const timer = setInterval(() => {
      setCount(count => count + 1)
    }, 1000)
    
    return () => {
      clearInterval(timer)
    }
  }, [])
`

可以看到,我们把count从依赖中取出,然后在setCount(count => count + 1),这样做得目的是为了告诉React,我们只需要count+1,并不需要读取它的值,因为React内部肯定是知道当前count的值,这样effect内部就不依赖count了,useEffect只需执行一次即可,这也是setState的函数写法,函数的参数就是最新的状态值,如果不太了解这种写法的朋友可以去查一下资料,这里就不再过多阐述了...

useEffect配合useReducer使用?

这就是useEffect究极使用技巧,用法及其广泛...还是使用上面的例子,让我们用useReducer改写一下

`function Demo() {
  const initState = { count: 0 }
  const [state, dispatch] = useReducer(reducer, initState)
  
  function reducer(state, action) {
    switch(action.type) {
      case 'increment' :
        return {count: state.count + 1}
      default: 
        throw new Error('type不存在...')
    } 
  }
  
  useEffect(() => {
   // 设定时器,每秒执行一次setCount
    const timer = setInterval(() => {
      dispatch({type: 'increment'})
    }, 1000)
    
    return () => {
      clearInterval(timer)
    }
  }, [dispatch]) // 这里我们依赖了dispatch 其实可以省略
  
  return (
    <div>
      <h2>{count}</h2>
      <button onClick={() => setCount(count + 1)}>count++</button>
    </div>
  ) 
}
`

大家可能会问我,这种写法的好处是什么?其实,我们利用useReducer通过action来描述行为,实现状态和行为的分离,在多个依赖的时候这种写法的优势就能很好的体现出来。

还有最后一个疑点:为什么dispatch可以省略?其实上面的例子,即使我们把dispatch从依赖中取出,也能正常运行,effect也只会执行一次。这么神奇?effect是怎么知道我们的行为是什么?其实,React内部会帮我们记住dispatch的各种行为(action),且能拿到最新的count,这一系列操作是React内部发生的,并不需要放在effect内。

useMemo和useCallback(性能优化)?

useCallback(缓存函数)

`const memoizedSetCount = useCallback(
  () => {
    setCount(count + 1)
  },
  [count],
);
`

把内联回调函数及依赖项数组作为参数传入useCallback,它将返回该回调函数的缓存版本,该回调函数仅在某个依赖项改变时才会更新。

useMemo(缓存值:类似于Vue的计算属性)

`const memoizedCount = useMemo(() => {
  const doubleCount = count * 2
}, [count]);
`

把“创建”函数和依赖项数组作为参数传入useMemo,它仅会在某个依赖项改变时才重新计算缓存值。这种优化有助于避免在每次渲染时都进行高开销的计算。

到这里,useEffect的进阶指南就介绍完了,要更好的运用React hooks 还得日常开发中的实战和积累,相信在往后的开发中,大家能更合理的使用hooks,也可以封装属于个人的hooks...大家加油,一起学习。
查看原文

赞 0 收藏 0 评论 0

叫我欧文就好 发布了文章 · 3月21日

useEffect进阶指南(上)

  1. 每次渲染都有独立的状态(State)

`function Demo() {
  const initCount = 0
  const [count, setCount] = useState(initCount)
  
  return (
    <div>
      <h2>{count}</h2>
      <button onClick={() => setCount(count + 1)}>count++</button>
    </div>
  ) 
}
`

每当用户点击一次按钮 都会重新触发render函数,每次render拿到的都是独立的状态

因为我们生命count的值时使用const,所以每次渲染拿到的count值是一个独立的常量。

  1. 每次渲染都有不同且独立的函数(Effect函数)

`function Demo() {
  const initCount = 0
  const [count, setCount] = useState(initCount)
  
  // 假设在1s内多次点击按钮 这里打印的count值是什么?
  useEffect(() => {
    setTimeout(() => {
      console.log(count) // 这里打印的会是当前这一次的count值,并不是最新的count值
    }, 1000)
  })
  
  return (
    <div>
      <h2>{count}</h2>
      <button onClick={() => setCount(count + 1)}>count++</button>
    </div>
  ) 
}
`

每次count值改变,都会触发render,组件重新渲染,所以每次都会生成对应的useEffect函数

而且我们发现每次打印count的值拿到的都是当前轮次的count值(并不是最新的count)

  1. useEffect到底是怎样拿到最新的状态值的?

我们知道每次渲染都会触发render,每次更新就会生成一个新的Effect函数,并且每一个Effect函数里面都有独立的State,且只能访问自己本次更新的State。

所以用上面的例子,得出的结论就是:count值其实不是在同一个Effect函数里面发生改变,而是每一次的组件更新,都会生成一个维护着本次更新的Effect函数,在这个最新的Effect函数里就可以访问到最新的count值。

  1. useEffect返回的函数是如何进行清理工作的?

`function Demo() {
  const initCount = 0
  const [count, setCount] = useState(initCount)
  
 
  useEffect(() => {
    let timer = setTimeout(() => {
      console.log(count)
    }, 1000)
    
    // 清理工作
    return () => {
      clearTimeout(timer)
    }
  })
  
  return (
    <div>
      <h2>{count}</h2>
      <button onClick={() => setCount(count + 1)}>count++</button>
    </div>
  ) 
}
`

假设用户点击了两次次按钮 当第一次点击的时候 count + 1 = 1,然后执行clearTimout清除本次的定时器? 接着继续count + 1 = 2 然后执行clearTimeout清除本次的定时器?

正确的顺序应该是:当第一次点击 count + 1 = 1,然后clearTimeout会被延迟执行,等到第二次点击的时候 count + 1 = 2 再执行上一次的clearTimeout 然后以此类推...问题来了 不是说effect函数只能访问本次的State吗?那它怎么拿到上一次的clearTimeout并执行的?

其实很简单,就是React会帮你记住每次effect函数的State(包括清除函数),它确实是只能读取本次更新的State,只不过是延迟执行了(把清除函数的执行时机放在DOM渲染完成后,在下一次render触发之前)

剩下的内容下期见吧,我累了,现在要去吃麦当劳补充一下能量...

下期预告:useEffect的第二个参数详细解析及使用,在开发中合理使用依赖(避免死循环、性能优化...)

查看原文

赞 0 收藏 0 评论 0

叫我欧文就好 发布了文章 · 3月19日

手把手教你React Hooks

1. useState

useState接收一个参数(状态的初始值)
useState返回值:一个数组 第一项:状态值 第二项:修改状态的方法
import { useState } from "react";

function Demo1() {
  // 定义count初始值
  const initCount = 0;
  
  const [count, setCount] = useState(initCount);

  const increment = () => {
    setCount((count) => count + 1);
    // 这里打印的count值并不是最新的 而是上一个count值 为什么?
    console.log(count);
  };
  return (
    <div>
      <h3>{count}</h3>
      <button onClick={() => setCount(count + 1)}>count++</button> &nbsp;
      <button onClick={increment}>count++</button>
    </div>
  );
}

export default Demo1;
因为每次状态更新,React内部都会重新生成一个useState函数,一次更新对应一个useState函数,所以最新的一次更新只能拿到上次更新完的state值,如果要拿到最新的状态值需要借助下一个hook(useEffect),继续往下看...

2. useEffect

useEffect相当于class组件的三个生命周期,分别是:

(componentDidMount、componentDidUpdate、componentWillUnmount)

import { useEffect, useState } from "react";

function Demo2() {
  const initCount = 0;
  const initName = "kyrie";

  const [count, setCount] = useState(initCount);
  const [name, setName] = useState(initName);

  const increment = () => {
    setCount((count) => count + 1);
  };

  const changeName = () => {
    setName("wen");
  };

  // componentDidUpdate - 调用时机:所有状态更新都会调用
  useEffect(() => {
    // console.log(count, "count");
  });
  
  // componentDidUpdate - 调用时机:依赖的状态更新才调用(也就是第二个参数传入的状态值(数组形式))
  useEffect(() => {
    // 只会在count值改变才调用
    console.log(count, "count"); // 1 2 3 4 5 ... 这里就能拿到最新的count值
  }, [count]);

  useEffect(() => {
    // 只会在count值改变才调用
    console.log(name, "name"); // wen name
  }, [name]);

  useEffect(() => {
    // count、name值改变都会调用 可传入多个状态
    console.log(count, "count"); // 1 2 3 4 5 ...
    console.log(name, "name"); // wen name
  }, [count, name]);

  // componentDidMount - 调用时机:初始化的时候调用一次,后续不再调用
  useEffect(() => {
    console.log(count, "count");
  }, []);
  
   // componentWillUnmount - 调用时机:组件销毁时调用
  useEffect(() => {
    console.log("useEffect");
    // 返回的函数会在组件销毁时调用
    return () => {
      console.log("组件销毁了");
    };
  });
  
  return (
    <div>
      <h3>{count}</h3>
      <h3>{name}</h3>
      <button onClick={increment}>count++</button> &nbsp;
      <button onClick={changeName}>changeName</button>
    </div>
  );
}

export default Demo2;

3. useRef

useRef接收一个初始值 它的返回值是一个对象,里面有个current属性{current: ...}
current属性的值就是实时的状态值
import { useRef } from "react";

const inputRef = useRef(null);

<input ref={inputRef} type="text" /> &nbsp;
// 这里inputRef.current拿到的是input框的DOM元素
<button onClick={() => inputRef.current.focus()}>点击聚焦</button>

4. useContext

useContext需要配合createContext使用
import { useContext, createContext } from "react";

// 创建一个上下文
const myContext = createContext();

// 提供者 值是通过它来进行传递的
const { Provider } = myContext;

const [theme, setTheme] = useState({ background: "white", color: "black" });

function toggleTheme() {
  theme.background === "white"
    ? setTheme({ background: "black", color: "white" })
    : setTheme({ background: "white", color: "black" });
}
// 父组件
// 这里把需要传给子孙组件的值放入value属性中
<Provider value={{ theme, toggleTheme }}>
    <Child />
</Provider>

// 子组件
// 子组件可以通过祖先组件的context拿到传递的值(包括方法)
const { theme, toggleTheme } = useContext(myContext);

return (
  <div>
    <h2>Child组件</h2>
    <button
      style={{ background: theme.background, color: theme.color }}
      onClick={() => toggleTheme(theme)}
    >
      切换颜色
    </button>
  </div>
);

5. useReducer(redux的思想)

useReducer接收两个参数,第一个参数为reducer函数,第二个参数为初始状态
useReducer返回值:第一项为store(即共享状态的集合)第二项dispatch(修改状态的方法)
import { useReducer } from "react";

// 定义一个reducer函数
function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return {
        num: state.num + action.count
      };
    case "decrement":
      return {
        num: state.num - action.count
      };
    default:
      return state;
  }
}

const [store, dispatch] = useReducer(reducer, { num: 100 });

<h3>num:{store.num}</h3>
<button onClick={() => dispatch({ type: "increment", count: 100 })}>+</button>
<button onClick={() => dispatch({ type: "decrement", count: 100 })}>-</button>

6. 自定义hooks(进阶)

// 自定义一个count++的hooks 方便复用 且hooks名字更加语义化
function useCount(initCount) {
  const [count, setCount] = useState(initCount);

  return [
    count,
    () => {
      setCount((count) => count + 1);
    }
  ];
}

const [count, addCount] = useCount(0);
<button onClick={() => {addCount()}> + </button>

本文属于基础内容,其实还有hooks的进阶使用,关于useEffect的进阶指南,后续我会单独写一遍文章供大家学习,相信大家看完这篇文章后,后续再看进阶指南的时候也会比较好理解...

查看原文

赞 0 收藏 0 评论 0

叫我欧文就好 发布了文章 · 3月18日

Vue3正确的打开方式(下)

1. 计算属性(computed)

computed 的返回值是一个响应式的 ref 对象

import { ref, computed } from 'vue'

const count = ref(10)

// 写法一:默认返回getter函数 返回值为一个只读的ref对象
const doubleCount = computed(() => count.value * 2)

// 因为返回值为ref对象 取值需要拿它的value
console.log(doubleCount.value) // 20

// 注意:这里控制台会报警告,因为我们只设置了getter函数
doubleCount.value = 20 // warn:readonly(只读)属性 不可修改

// 写法二:带有getter和setter函数
const doubleCount = computed({
  get: () => count.value * 2,
  set: (val) => count.value = val - 2
})

console.log(doubleCount.value) // 20

//  不会报警告,因为我们手动设置了setter函数
doubleCount.value = 10  // 修改doubleCount的值 触发set函数

// 所以count的值为 10 - 2 = 8
console.log(count.value) // 8

2. 响应式侦听(watch)

①. 监听基本数据类型:

import { ref, watch, reactive } from 'vue'

setup() {

  // 写法一:监听一个普通的值
  const data = reactive({ count: 100 })
  // 第一个参数写成函数
  watch(() => data.count, (newVal, oldVal) => {
    console.log(newVal, 'newVal')
    console.log(oldVal, 'oldVal')
  })

  // 写法二:监听ref对象
  const count = ref(100)
  // 第一个参数写成函数
  watch(() => count, (newVal, oldVal) => {
    console.log(newVal, 'newVal')
    console.log(oldVal, 'oldVal')
  })

  // 写法三:同时监听多个值
  const num = ref(200)
  // 第一个参数写成数组形式,代表要监听的值
  watch([count, num], (newVals, oldVals) => {
    // 这里打印的是一个数组 值分别对应:[count, num]
    console.log(newVals, 'newVals')
    console.log(oldVals, 'oldVals')
  })

  count.value = 300 // newVals: [300, 200]   oldVals: [100, 200]
  num.value = 400   // newVals: [300, 400]   oldVals: [300, 200]
}

②. 监听引用数据类型:

import { watch, reactive } from 'vue'

setup() {
  const list = reactive([1, 2, 3, 4, 5])
  // 这里需要对引用数据类型进行拷贝
  watch(() => [...list], (newVal, oldVal) => {
    console.log(newVal, 'newVal')
    console.log(oldVal, 'oldVal')
  })

  list.push(6) // newVal: [1, 2, 3, 4, 5, 6]  oldVal: [1, 2, 3, 4, 5]
}
不出意外,vue3 的 watch 同样支持 deep、immediate 属性,只是用法有所改变
const userInfo = reactive({
  username: 'kyrie irving',
  email: '123@qq.com',
  like: {
    isBasketball: true
  }
})

// 传入一个对象作为第三个参数 可开启deep/immediate
watch(() => userInfo, (newVal, oldVal) => {
  console.log(newVal.like.isBasketall)
  console.log(oldVal.like.isBasketall)
}, { deep: true })

// 修改状态深层属性的值
userInfo.like.isBasketball = false // 打印日志: false, false
看到日志输出两个 false 是不是觉得哪里不对劲?
注意:如果你想要在修改后的状态中拿到上一个状态里的值,需要深拷贝
原因:watch 的返回值是:当前状态(newVal)和上一个状态的引用(oldVal)
// 正确写法(对引用数据类型进行深拷贝)
(这里使用vue官方推荐的lodash深拷贝方法,当然你也可以手写(手动狗头)
import _ from 'lodash'

watch(() => _.cloneDeep(userInfo), (newVal, oldVal) => {
  console.log(newVal.like.isBasketall)
  console.log(oldVal.like.isBasketall)
})

// 修改状态深层属性的值 这里就可以拿到上一个状态的值啦
userInfo.like.isBasketball = false // 打印日志: false, true

3. 组合式 API(核心知识点)

假设我们要开发一个 todoList, 那么在 vue2 的选项式(options)API 中,代码大致长这样:
export default {
  components: ['Header', 'List', 'Footer'],

  props: {
    item: {
      type: Object,
      required: true,
      default: () => {},
    }
  },

  data() {
    return {
      value: '',
    }
  },

  computed: {
    finallyValue() {}
  },

  methods: {
    addTodo(data) {}
  },

  mounted() {
    this.init()
  }
}

很明显可以看到,我们写的代码被瓜分到(data,computed, methods, mounted...)里面,当组件小的时候还好,但当我们开发一个大型组件的时候,就会发现我们的组件变得难以维护

不知道大家有没有这种体验:当我们的组件代码行数太多时,我们时常需要不断地上下‘跳转’编译器,找到相关的代码块去阅读或编写,这显然开发体验不是很好。

要是有一种东西可以解决这种开发中由于代码太过于碎片化,而且逻辑不好复用的问题,是不是可以提高我们的开发效率?而这正是 vue3 推出组合式 API出现的原因。

Vue3 正确的打开方式(上)里,我们已经用到组合式 API 了,定义响应式对象(ref, reactive)...

补充一些关于生命周期props的知识...

生命周期(vue3将不再需要:beforeCreate、created,原因看下文)
// vue3中为了性能,很多API(方法)都支持Tree Shaking 
// 所以需要从vue中显式导入,生命周期也不例外
import { 
  onBeforeMount,      (对应beforeMount)
  onMounted,          (对应mounted)
  onBeforeUpdate,     (对应beforeUpdate)
  onUpdated,          (对应updated)
  onBeforeUnmount,    (对应beforeUnmount)
  onUnmounted,        (对应unmounted)
  onErrorCaptured,    (对应errorCaptured)   基本不用
  onRenderTracked,    (对应renderTracked)   基本不用
  onRenderTriggered   (对应renderTriggered) 基本不用
} from 'vue'

// 第一个参数props,第二个参数为上下文(包括slots,attrs,emit)
setup(props, context) {

  // 用法跟vue类似,不一样的地方在于它接受一个回调函数,在钩子被组件调用时执行
  onMounted(() => {
    console.log('mouted')
  }),
  
  // 其他的钩子类似,这里就不一一列举了
  ...
}

tips: vue3的setup是围绕beforeCreatecreated生命钩子运行的,所以你不需要显式地定义这两个钩子函数,通俗易懂就是说之前在这两个钩子里编写的代码现在你都应该编写在setup函数中。

props(响应式引用)

import { toRefs } from 'vue'

// 接收跟vue2一样 可进行类型校验和默认值设置
props: {
  username: {
    type: String,
    default: ''
  },
  email: {
    type: String,
    default: ''
  }
}

// 第一个参数props,第二个参数为上下文(包括slots, attrs, emit)
// 所以你可以使用解构的写法,需要注意的是:attrs和slots是有状态的对象
// 所以你应该以attrs.x 或 slots.xx 来使用,且它们是非响应式的。
setup(props, { slots, attrs, emit }) {

 // 注意:解构会丢失响应式
  const { username, email } = props
  
  console.log(username, email)

  // 需要调用toRefs()解决响应式丢失问题
  const { username, email } = toRefs(props)
}

4. Provide / Inject

provide(name, value) name(String类型)
inject(name, 默认值(可选)) inject的name和provide的name要相对应

Parent.vue 父组件

<template>
  <div>
    <h1>App</h1>
    <p>App的count:{{ count }}</p>
    <p>App的用户名:{{ userInfo.username }}</p>
    // 子孙组件
    <Child />
  </div>
</template>

<script>
// 先引入provide
import { provide } from 'vue'

  setup() {

    // 建议把provide的状态转换为响应式(ref / reactive)
    const count = ref(100);

    const userInfo = reactive({
      username: "kyrie",
      email: "123@qq.com",
    });

    // 如果需要修改provide里面的状态 推荐在父组件注入修改状态的方法
    const changeCount = () => {
      count.value++;
    };

    const changeUsername = () => {
      userInfo.username = "wen";
    };

    // 为了避免inject组件修改provide的状态 使用readonly确保数据不被inject组件修改
    provide("count", readonly(count));
    provide("userInfo", readonly(userInfo));

    provide("changeCount", changeCount);
    provide("changeUsername", changeUsername);
  }
</script>

Child.vue 组件

<template>

  <h2>Child</h2>
  <p>inject的count:{{ count }}</p>
  <p>inject的用户名:{{ userInfo.username }}</p>
  <p>inject的邮箱:{{ userInfo.email }}</p>
  
  // 调用inject的changeCount方法修改count
  <button @click="changeCount">count++</button> &nbsp;
  // 调用inject的changeUsername方法修改用户名
  <button @click="changeUsername">修改用户名</button>
  
</template>

<script>
import { inject } from "vue";
export default {
  setup(props, context) {
    // 注入count 并设置count的默认值为0
    const count = inject("count", 0);
    // 注入userInfo 并设置userInfo的默认值为0
    const userInfo = inject("userInfo", {});
    // 注入修改count的方法
    const changeCount = inject("changeCount");
    // 注入修改userInfo的方法
    const changeUsername = inject("changeUsername");
    return {
      count,
      userInfo,
      changeCount,
      changeUsername,
    };
  },
};
</script>

以上就是Vue3常用的也算比较核心的API了,如果你有什么疑惑或者问题,欢迎在下方留言哦...

查看原文

赞 0 收藏 0 评论 0

叫我欧文就好 发布了文章 · 3月17日

Vue3正确的打开方式(上)

1. Vue3 定义响应式数据(ref/reactive)

import { reactive, toRefs } from 'vue'
export default {

  setup() {
    const data = reactive({
      title: 'Hello zbw',
      todoList: [
        {
          id: 1,
          name: 'kyrie',
          age: 18
        },
        {
          id: 2,
          name: 'wen',
          age: 18
        }
      ]
    })

    // 1、toRefs:普通对象转换为响应式对象
    const refData = toRefs(data)
    
    ///2、或者你可以使用ref定义
    const name = ref('kyrie')
    
    // 使用ref定义的数据 取的时候要取它的value
    conosle.log(name.value) // 'kyrie'
    
    // 修改的时候也一样
    name.value = 'wen'

    return {
      ...refData
    }
  }

}

2. Vue3 定义和使用全局属性(方法)

vue.prototype 替换成 config.globalProperties

// main.js 全局定义

import { createApp } from "vue";
import App from "./App.vue";

const app = createApp(App);

// 定义debounce方法 
const debounce = (fn, delay) => {
  let timer;
  return (...args) => {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
};

//把debounce方法挂载到原型上
app.config.globalProperties.$debounce = debounce;

// .vue 组件中使用

import { getCurrentInstance } from 'vue'

// 通过getCurrentInstance拿到当前实例 并取出debounce
const { proxy: { debounce } } = getCurrentInstance()

// 可在组件内使用
onMounted(() => {
  data.onSubmit = debounce(data.onSubmit, 500)
})

3. Vue3 自定义指令(全局)

import { createApp } from "vue";
import App from "./App.vue";

const app = createApp(App);

// 全局注册一个高亮显示文本的指令
app.directive("highlight", {
  // vue3修改了钩子的名字,更加语义化
  beforeMount(el, binding, node) {
    el.style.color = binding.value;
  },
});

// .vue 组件内使用自定义指令
<p v-highlight="pColor">高亮显示此文本为红色</p>

<i v-highlight="iColor">高亮显示此文本为绿色</i>

setup() {
  let pColor = ref('red')
  let iColor = ref('green')

  return {
    pColor,
    iColor
  }
}

4. Vue3 父子通信($emit的改动)

vue3新增了$emit的options(配置)emits: [] / {} 可用于对传给父组件的值进行校验

// Child组件

// 注意:这里$emit的事件名不要写驼峰形式,html无法解析驼峰
<button @click="$emit('say-hello', { msg: 'Hello', count: 1 })">say Hello</button>

export default {
  // 这里可以写成数组形式,跟vue2一样,无参数校验
  // emits: ['say-hello']
  // 如果需要对传递给父组件的值进行校验,可以写成对象形式
  emits: {
    // payload为传递给父组件的值
    'say-hello': payload => {
      if (payload.msg) {
        console.log('参数校验通过!')
        return true
      } else {
        console.log('参数缺少msg')
        return false
      }
    }
  }
}

// Parent组件
<Child @sayHello="sayHi"/>

setup() {
  sayHi(val) {
    console.log(val, '子组件传过来的值')
  }
}

5. Vue3 兄弟组件通信(mitt)

// emit.js
import mitt from "mitt";
export const emitter = mitt();

// Child1.vue
import { emitter } from "../emit";

//第一个参数:事件名,第二个参数:传递的值
emitter.emit('changeTitle', '新标题')

// Child2.vue
import { emitter } from "../emit";

// 组件内部的值
const data = reactive({
  title: "旧标题"
})

//value可以接收到Child1传过来的值
emitter.on('changeTitle', (value) => {
  // 修改组件内部title
  data.title = value
})

// 如果发射多个事件,可以使用这种写法
// * 代表监听全部事件 type:发射的事件名,val为传过来的值
emitter.on("*", (type, val) => {
  switch (type) {
    case "increment":
      data.count += val;
      break;
    case "decrement":
      data.count -= val;
      break;
    case "changeTitle":
      data.title = val;
      break;
    default:
      break;
  }
});

// 注意:如果你不想定义emit.js 可以将emitter挂载到原型上

6. Provide / Inject的使用(vue2版)

// main.js

app.provide("userInfo", {
  username: "kyrie",
  password: "123456"
});

// .vue 组件内使用
inject: {
    myUserInfo: {
      from: 'userInfo' // { username: 'kyrie', password: '123456'}
    }
  },
查看原文

赞 0 收藏 0 评论 0

叫我欧文就好 赞了文章 · 3月11日

理解 JavaScript 的 async/await

2020-06-04 更新

JavaScript 中的 async/await 是 AsyncFunction 特性 中的关键字。目前为止,除了 IE 之外,常用浏览器和 Node (v7.6+) 都已经支持该特性。具体支持情况可以在 这里 查看。


我第一次看到 async/await 这组关键字并不是在 JavaScript 语言里,而是在 C# 5.0 的语法中。C# 的 async/await 需要在 .NET Framework 4.5 以上的版本中使用,因此我还很悲伤了一阵——为了要兼容 XP 系统,我们开发的软件不能使用高于 4.0 版本的 .NET Framework。

我之前在《闲谈异步调用“扁平”化》 中就谈到了这个问题。无论是在 C# 还是 JavaScript 中,async/await 都是非常棒的特性,它们也都是非常甜的语法糖。C# 的 async/await 实现离不开 Task 或 Task\<Result\> 类,而 JavaScript 的 async/await 实现,也离不开 Promise

现在抛开 C# 和 .NET Framework,专心研究下 JavaScript 的 async/await。

1. async 和 await 在干什么

任意一个名称都是有意义的,先从字面意思来理解。async 是“异步”的简写,而 await 可以认为是 async wait 的简写。所以应该很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。

另外还有一个很有意思的语法规定,await 只能出现在 async 函数中。然后细心的朋友会产生一个疑问,如果 await 只能出现在 async 函数中,那这个 async 函数应该怎么调用?

如果需要通过 await 来调用一个 async 函数,那这个调用的外面必须得再包一个 async 函数,然后……进入死循环,永无出头之日……

如果 async 函数不需要 await 来调用,那 async 到底起个啥作用?

1.1. async 起什么作用

这个问题的关键在于,async 函数是怎么处理它的返回值的!

我们当然希望它能直接通过 return 语句返回我们想要的值,但是如果真是这样,似乎就没 await 什么事了。所以,写段代码来试试,看它到底会返回什么:

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

const result = testAsync();
console.log(result);

看到输出就恍然大悟了——输出的是一个 Promise 对象。

c:\var\test> node --harmony_async_await .
Promise { 'hello async' }

所以,async 函数返回的是一个 Promise 对象。从文档中也可以得到这个信息。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

补充知识点 [2020-06-04]

Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。

async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,我们当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样

testAsync().then(v => {
    console.log(v);    // 输出 hello async
});

现在回过头来想下,如果 async 函数没有返回值,又该如何?很容易想到,它会返回 Promise.resolve(undefined)

联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。

那么下一个关键点就在于 await 关键字了。

1.2. await 到底在等啥

一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。

因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行

function getSomething() {
    return "something";
}

async function testAsync() {
    return Promise.resolve("hello async");
}

async function test() {
    const v1 = await getSomething();
    const v2 = await testAsync();
    console.log(v1, v2);
}

test();

1.3. await 等到了要等的,然后呢

await 等到了它要等的东西,一个 Promise 对象,或者其它值,然后呢?我不得不先说,await 是个运算符,用于组成表达式,await 表达式的运算结果取决于它等的东西。

如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。

如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

看到上面的阻塞一词,心慌了吧……放心,这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。

2. async/await 帮我们干了啥

2.1. 作个简单的比较

上面已经说明了 async 会将其后的函数(函数表达式或 Lambda)的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来。

现在举例,用 setTimeout 模拟耗时的异步操作,先来看看不用 async/await 会怎么写

function takeLongTime() {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), 1000);
    });
}

takeLongTime().then(v => {
    console.log("got", v);
});

如果改用 async/await 呢,会是这样

function takeLongTime() {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), 1000);
    });
}

async function test() {
    const v = await takeLongTime();
    console.log(v);
}

test();

眼尖的同学已经发现 takeLongTime() 没有申明为 async。实际上,takeLongTime() 本身就是返回的 Promise 对象,加不加 async 结果都一样,如果没明白,请回过头再去看看上面的“async 起什么作用”。

又一个疑问产生了,这两段代码,两种方式对异步调用的处理(实际就是对 Promise 对象的处理)差别并不明显,甚至使用 async/await 还需要多写一些代码,那它的优势到底在哪?

2.2. async/await 的优势在于处理 then 链

单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。

假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。我们仍然用 setTimeout 来模拟异步操作:

/**
 * 传入参数 n,表示这个函数执行的时间(毫秒)
 * 执行的结果是 n + 200,这个值将用于下一步骤
 */
function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}

function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

现在用 Promise 方式来实现这三个步骤的处理

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms

输出结果 resultstep3() 的参数 700 + 200 = 900doIt() 顺序执行了三个步骤,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 计算的结果一致。

如果用 async/await 来实现呢,会是这样

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();

结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样

2.3. 还有更酷的

现在把业务要求改一下,仍然是三个步骤,但每一个步骤都需要之前每个步骤的结果。

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(m, n) {
    console.log(`step2 with ${m} and ${n}`);
    return takeLongTime(m + n);
}

function step3(k, m, n) {
    console.log(`step3 with ${k}, ${m} and ${n}`);
    return takeLongTime(k + m + n);
}

这回先用 async/await 来写:

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time1, time2);
    const result = await step3(time1, time2, time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();

// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 800 = 300 + 500
// step3 with 1800 = 300 + 500 + 1000
// result is 2000
// doIt: 2907.387ms

除了觉得执行时间变长了之外,似乎和之前的示例没啥区别啊!别急,认真想想如果把它写成 Promise 方式实现会是什么样子?

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => {
            return step2(time1, time2)
                .then(time3 => [time1, time2, time3]);
        })
        .then(times => {
            const [time1, time2, time3] = times;
            return step3(time1, time2, time3);
        })
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

有没有感觉有点复杂的样子?那一堆参数处理,就是 Promise 方案的死穴—— 参数传递太麻烦了,看着就晕!

3. 洗洗睡吧

就目前来说,已经理解 async/await 了吧?但其实还有一些事情没提及——Promise 有可能 reject 啊,怎么处理呢?如果需要并行处理3个步骤,再等待所有结果,又该怎么处理呢?

阮一峰老师已经说过了,我就懒得说了。

4. 推荐相关文章

5. 来跟边城(作者)学 更新@2020-11-14

TypeScript从入门到实践 【2020 版】

TypeScript从入门到实践 【2020 版】

6. 关于转载 补充@2020-03-05

常有读者问是否可以转载。

笔者表示欢迎各位转载,但转载时一定注明作者和出处,谢谢!


公众号-边城客栈
请关注公众号 边城客栈

看完了先别走,点个赞啊 ⇓,赞赏 ⇘ 也行!

查看原文

赞 1345 收藏 1132 评论 134

叫我欧文就好 关注了标签 · 3月8日

html5

HTML5 是 HTML 下一个的主要修订版本,现在仍处于发展阶段。广义论及 HTML5 时,实际指的是包括 HTML、CSS 和 JavaScript 在内的一套技术组合。

关注 90937

叫我欧文就好 关注了标签 · 3月8日

css3

层叠样式表(英语:Cascading Style Sheets,简写CSS),又称串样式列表,由W3C定义和维护的标准,一种用来为结构化文档(如HTML文档或XML应用)添加样式(字体、间距和颜色等)的计算机语言。目前最新版本是CSS2.1,为W3C的候选推荐标准。CSS3现在已被大部分现代浏览器支持,而下一版的CSS4仍在开发过程中。

关注 23367

叫我欧文就好 关注了标签 · 3月8日

es6

ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

标准的制定者有计划,以后每年发布一次标准,使用年份作为版本。因为 ES6 的第一个版本是在 2015 年发布的,所以又称ECMAScript 2015(简称 ES2015)。

2016 年 6 月,小幅修订的《ECMAScript 2016 标准》(简称 ES2016)如期发布。由于变动非常小(只新增了数组实例的includes方法和指数运算符),因此 ES2016 与 ES2015 基本上是同一个标准,都被看作是 ES6。根据计划,2017 年 6 月将发布 ES2017。

标准请参读 ECMAScript® 2015 Language Specification

关注 2665

叫我欧文就好 关注了标签 · 3月8日

webpack

Webpack 是一个前端资源加载/打包工具,只需要相对简单的配置就可以提供前端工程化需要的各种功能。

关注 4078