叫我欧文就好

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

广州编辑  |  填写毕业院校广州凡岛网络有限公司  |  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中的作用域和闭包,这些知识都是进阶必备的,如果有不理解的,花时间多看几遍,相信你一定可以掌握其中的精髓。

都到这儿了!

点个关注再走呗!!

查看原文

赞 8 收藏 5 评论 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

认证与成就

  • 获得 68 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-07-26
个人主页被 1.1k 人浏览