JS里的原型到底是个什么东西

首先要明确下面2点

  • 虽然js里有class这个东西,也可以近似像java那样写关于类的代码,这是没错的
  • 但是js里是不存在类的,它的本质其实是构造函数和原型链的语法糖

先来看一个基本的构造函数

1
2
3
4
5
6
function Person(name = '人') {
this.name = name
}

const p1 = new Person('小明')
console.log(p1) // Person { name: '小明' }

现在给构造函数中的this添加一个方法:say()

可以看到,两个实例p1p2say方法是不相等的,而每创建一个实例,都会额外创建一个新的函数,这样显然是不好的,需要有一种方法,使得创建的实例的方法是共享的

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name = '人') {
this.name = name
this.say = function() {
console.log(`大家好,我是:${this.name}`)
}
}

const p1 = new Person('小明')
const p2 = new Person('小刚')
console.log(p1, p2) // Person { name: '小明', say: [Function] } Person { name: '小刚', say: [Function] }
p1.say() // 大家好,我是:小明
p2.say() // 大家好,我是:小刚
console.log(p1.say === p2.say) // false

于是,原型对象就出现了

js中一切都是对象,所以函数也是对象,所有的函数都会拥有一个prototype属性,这个属性指向的对象是这个函数的原型对象,构造函数的原型对象上的属性和方法都会被它的实例所继承

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name = '人') {
this.name = name
}
Person.prototype.say = function() {
console.log(`大家好,我是:${this.name}`)
}

const p1 = new Person('小明')
const p2 = new Person('小刚')
console.log(p1, p2) // Person { name: '小明' } Person { name: '小刚' }
p1.say() // 大家好,我是:小明
p2.say() // 大家好,我是:小刚
console.log(p1.say === p2.say) // true

而原型对象上有一个属性constructor,保存的是它的那个构造函数的引用

所以下面可以看到Person.prototype把原型对象给指了出来,但又被Person.prototype.constructor给指了回去

1
2
3
4
5
function Person(name = '人') {
this.name = name
}

console.log(Person.prototype.constructor = Person) // true

那构造函数的实例有什么办法可以获取到生成它的构造函数吗?

是可以的,每个实例上都会有constructor属性,直接指向了它的构造函数的原型对象,同时呢每个实例上都会有一个__proto__属性,直接指向了它的构造函数的原型对象,再通过constructor指回来它的构造函数就行

1
2
3
4
5
6
7
function Person(name = '人') {
this.name = name
}
const p1 = new Person('小明')
console.log(p1 instanceof Person) // true
console.log(p1.constructor === Person) // true
console.log(p1.__proto__.constructor === Person) // true

所以呢这就形成了一个环,如下图
原型链图

但最终原型链应该是一个“链”

也就是说可以顺着原型对象一直指下去,对的

原型对象本身也具有__proto__属性,直接指向了它的构造函数,当一个实例调用它的方法在它的构造函数中找不到时,就回去原型对象上去找,当原型对象找到不的时候就会去原型对象的原型对象上去找,直到原型链的尽头。

Promises/A+ 规范

一个开放标准,对于开发人员可互操作的 JavaScript 承诺

一个 promise 代表一个异步操作的最终结果。主要的操作方式是通过调用 promise 的 then 方法,它接受的回调函数接受 promise 成功的结果或失败的原因

这个规范详细的描述了 then 方法的行为,提供一个互操作基础,所有符合 Promises/A+ 的都可以依赖这个标准实现。因此,该规范已经十分稳定。尽管 Promises/A+ 组织可能会偶尔修改以实现向后兼容,我们也会整合这些大的或不能向后兼容的改变,一起研究,讨论,测试。

曾经, Promises/A+ 解释了早期 PromisesA 提议的条款,扩展了事实上的行为和忽略了不标准和有问题的部分。

最终,Promises/A+ 规范并没处理如何创建 fulfill,或 reject promise,而选择了可互操作的 then 方法替代。在今后的工作中可能会考虑。

1. 术语

1.1 promise 是一个有符合此标准的 then 方法的 objectfunction

1.2 thenablethen 方法定义的 objectfunction

1.3 value 是一个 JavaScript 合法值(包括 undefined,thenable,promise)

1.4 exception 是一个 throw 语句抛出错误的值

1.5 reason 是一个表明 promise 失败的原因的值

2. 要求

2.1 Promise 状态

一个 promise 有且只有一个状态(pending,fulfilled,rejected 其中之一)

2.1.1 pending 状态时:

  • 2.1.1.1 可能会转变为 fulfilled 或 rejected 状态

2.1.2 fulfilled 状态时:

  • 2.1.2.1 不能再状态为任何其他状态

  • 2.1.2.2 必须有一个 value,且不可改变

2.1.3 rejected 状态时:

  • 2.1.3.1 不能再状态为任何其他状态

  • 2.1.3.2 必须有一个 reason,且不可改变

注:这里 不可改变 意思是不可变恒等(同理 === ),但不意味永远不可变

2.2 then 方法

一个 promise 必须提供一个 then 方法,用来获取当前或最终的 value 或 reason

一个 promise 的 then 方法接受两个参数:

promise.then(onFulfilled, onRejected)

2.2.1 onFulfilled 和 onRejected 都是可选参数:

  • 2.2.1.1 如果 onFulfilled 不是函数,它会被忽略

  • 2.2.1.2 如果 onRejected 不是函数,它会被忽略

2.2.2 如果 onFulfilled 是一个函数:

  • 2.2.2.1 它一定是在 promise 是 fulfilled 状态后调用,并且接受一个参数 value

  • 2.2.2.2 它一定是在 promise 是 fulfilled 状态后调用

  • 2.2.2.3 它最多被调用一次

2.2.3 如果 onRejected 是一个函数:

  • 2.2.3.1 它一定在 promise 是 rejected 状态后调用,并且接受一个参数 reason

  • 2.2.3.2 它一定在 promise 是 rejected 状态后调用

  • 2.2.3.3 它最多被调用一次

2.2.4 onFulfilled 或 onRejected 只在执行环境堆栈只包含平台代码之后调用 [3.1]

2.2.5 onFulfilled 和 onRejected 会作为函数形式调用 (也就是说,默认 this 指向 global,严格模式 undefined) [3.2]

2.2.6 在同一个 promise 实例中,then 可以链式调用多次

  • 2.2.6.1 如果或当 promise 转态是 fulfilled 时,所有的 onFulfilled 回调回以他们注册时的顺序依次执行

  • 2.2.6.2 如果或当 promise 转态是 rejected 时,所有的 onRejected 回调回以他们注册时的顺序依次执行

2.2.7 then 方法一定返回一个 promise

promise2 = promise1.then(onFulfilled, onRejected);

  • 2.2.7.1 如果 onFulfilled 或 onRejected 返回的是一个 x,那么它会以

[[Resolve]](promise2, x) 处理解析

  • 2.2.7.2 如果 onFulfilled 或 onRejected 里抛出了一个异常,那么 promise2 必须捕获这个错误(接受一个 reason 参数)

  • 2.2.7.3 如果 onFulfilled 不是一个函数,并且 promise1 状态是 fulfilled,那么 promise2 一定会接受到与 promse1 一样的值 value

  • 2.2.7.4 如果 onRejected 不是一个函数,并且 promise1 状态是 rejected,promise2 一定会接受到与 promise1 一样的值 reason

2.3 Promise 处理程序

promise 处理程序是一个表现形式为 [[Resolve]](promise, x) 的抽象处理操作。如果 x 是 thenable 类型,它会尝试生成一个 promise 处理 x,否则它将直接 resolve x

只要 then 方法符合 Promises/A+ 规则,那么对 thenables 处理就允许实现可互操作(链式调用,层层传递下去)。它也允许对那些不符合 Promises/A+ 的 then 方法进行 “吸收”

[[Resolve]](promise, x) 的执行表现形式如下步骤:

2.3.1 如果返回的 promise1 和 x 是指向同一个引用(循环引用),则抛出错误

2.3.2 如果 x 是一个 promise 实例,则采用它的状态:

  • 2.3.2.1 如果 x 是 pending 状态,那么保留它(递归执行这个 promise 处理程序),直到 pending 状态转为 fulfilled 或 rejected 状态

  • 2.3.2.2 如果或当 x 状态是 fulfilled,resolve 它,并且传入和 promise1 一样的值 value

  • 2.3.2.3 如果或当 x 状态是 rejected,reject 它,并且传入和 promise1 一样的值 reason

2.3.3 此外,如果 x 是个对象或函数类型

  • 2.3.3.1 把 x.then 赋值给 then 变量

  • 2.3.3.2 如果捕获(trycatch)到 x.then 抛出的错误的话,需要 reject 这个promise

  • 2.3.3.3 如果 then 是函数类型,那个用 x 调用它(将 thenthis 指向 x),第一个参数传 resolvePromise ,第二个参数传 rejectPromise:

    • 2.3.3.3.1 如果或当 resolvePromise 被调用并接受一个参数 y 时,执行
      [[Resolve]](promise, y)

    • 2.3.3.3.2 如果或当 rejectPromise 被调用并接受一个参数 r 时,执行 reject(r)

    • 2.3.3.3.3 如果 resolvePromise 和 rejectPromise 已经被调用或以相同的参数多次调用的话吗,优先第一次的调用,并且之后的调用全部被忽略(避免多次调用)

    • 2.3.3.4 如果 then 执行过程中抛出了异常,

      • 2.3.3.3.4.1 如果 resolvePromise 或 rejectPromise 已经被调用,那么忽略异常

      • 2.3.3.3.4.2 否则,则 reject 这个异常

  • 2.3.3.4 如果 then 不是函数类型,直接 resolve x(resolve(x))

2.3.4 如果 x 即不是函数类型也不是对象类型,直接 resolve x(resolve(x))

如果被 resolve 的 promise 参与了 thenable 的循环链中,那么可能会导致无限递归。我们鼓励实现检测这种无限递归的方法并且返回一个错误信息,但并不是必须的 [3.6]

3. 备注

3.1 这里的 “平台代码”是指引擎,环境,和 promise 实现代码。实际上,这个要求确保 onFulfilled 和 onRejected 都在下一轮的事件循环中(一个新的栈)被异步调用。可以用宏任务,例如:setTimeoutsetImmediate 或者微任务,例如:MutationObseverprocess.nextTick 实现。 由于 promise 的实现被当做平台代码,所以它本身可能包含一个任务队列或 “trampoline” 的处理程序

3.2 这个 this 在严格模式下是 undefined,在宽松模式,指向 global 对象

3.3 具体的实现可以允许 promise2 和 promise1 绝对相等,要满足所有要求。每一个处理 promise2 和 promise1 绝对相等的实现都要写上文档标注

3.4 通常,只有它来自当前实现才可以判断 x 是一个真正的 promise。 此条款允许采取已知符合 promise 标准实现的状态

3.5 把 x.then 存起来,然后测试、调用这个引用,避免多次访问 x.then 属性。这么做的原因是防止每次获取 x.then 时,返回不同的情况(ES5getter 特性可能会产生副作用)

3.6 实现不应该武断地限制 thenable 链的深度,假设超出限制的无限递归。只有真正的循环引用才会导致一个 TypeError 错误,如果遇到一个不同的无限递归 thenable 链,一直递归永远是正确的行为

参考资料

【译】 Promises/A+ 规范

注:本文不以盈利为目的,仅做学习交流使用,若有侵权,请联系我删除,万分感谢!

JS的一些小技巧

类型强制转换

string 强制转换为数字

可以用*来转化为数字 (实际上是调用.valueOf方法)

然后使用Number.isNaN来判断是否为NaN,或者使用 a !== a 来判断是否为NaN,因为 NaN !== NaN

1
2
3
4
5
'32' * 1 // 32
'ds' * 1 // NaN
null * 1 // 0
undefined * 1 // NaN
1 * { valueOf: () => '3' } // 3

常用:也可以使用+来转化字符串为数字

1
2
3
4
5
6
+ '123' // 123
+ 'ds' // NaN
+ '' // 0
+ null // 0
+ undefined // NaN
+ { valueOf: () => '3' } // 3

object 强制转化为 string

可以使用 字符串 +Object 的方式来转化对象为字符串 (实际上是调用 .toString() 方法)

1
2
'the Math object:' + Math // "the Math object:[object Math]"
'the JSON object:' + JSON // "the JSON object:[object JSON]"

当然也可以覆盖对象的toString和valueOf方法来自定义对象的类型转换:

1
2
2  * { valueOf: () => '3' } // 6
'J' + { toString: () => 'S' } // "JS"

《Effective JavaScript》P11:当+用在连接字符串时,当一个对象既有toString方法又有valueOf方法时候,JS 通过盲目使用valueOf方法来解决这种含糊。

对象通过valueOf方法强制转换为数字,通过toString方法强制转换为字符串

1
'' + { toString: ()=> 'S', valueOf: () => 'J' } // J

使用 Boolean 过滤数组中的所有假值

我们知道 JS 中有一些假值:false,null,0,””,undefined,NaN,怎样把数组中的假值快速过滤呢,可以使用 Boolean 构造函数来进行一次转换

1
2
const compact = arr => arr.filter(Boolean)
compact([0, 1, false, 2, '', 3, 'a', 'e' * 23, NaN, 's', 34]) // [ 1, 2, 3, 'a', 's', 34 ]

双位运算符 ~~

可以使用双位操作符来替代 Math.floor( )。双否定位操作符的优势在于它执行相同的操作运行速度更快。

1
2
3
Math.floor(4.9) === 4 //true
// 简写为:
~~4.9 === 4 //true

不过要注意,对整数来说 ~~ 运算结果与 Math.floor( ) 运算结果相同,而对于负数来说不相同:

1
2
3
4
~~4.5 // 4
Math.floor(4.5) // 4
~~-4.5 // -4
Math.floor(-4.5) // -5

短路运算符

我们知道逻辑与&&与逻辑或||是短路运算符,短路运算符就是从左到右的运算中前者满足要求,就不再执行后者了;
可以理解为:

  • &&为取假运算,从左到右依次判断,如果遇到一个假值,就返回假值,以后不再执行,否则返回最后一个真值。

  • ||为取真运算,从左到右依次判断,如果遇到一个真值,就返回真值,以后不再执行,否则返回最后一个假值。

因此可以用来做很多有意思的事,比如给变量赋初值:

1
2
let variable1
let variable2 = variable1 || 'foo'

如果 variable1 是真值就直接返回了,后面短路就不会被返回了,如果为假值,则会返回后面的foo。

也可以用来进行简单的判断,取代冗长的if语句:

1
let variable = param && param.prop

如果param如果为真值则返回param.prop属性,否则返回param这个假值,这样在某些地方防止param为undefined的时候还取其属性造成报错。

取整 | 0

对一个数字| 0可以取整,负数也同样适用,num | 0

1
2
1.3 | 0 // 1
-1.9 | 0 // -1

判断奇偶数 & 1

对一个数字& 1可以判断奇偶数,负数也同样适用,num & 1

1
2
3
const num = 3;
!!(num & 1) // true
!!(num % 2) // true

函数

函数默认值

1
2
func = (l, m = 3, n = 4 ) => (l * m * n);
func(2) //output: 24

注意,传入参数为undefined或者不传入的时候会使用默认参数,但是传入null还是会覆盖默认参数。

强制参数

默认情况下,如果不向函数参数传值,那么 JS 会将函数参数设置为undefined。其它一些语言则会发出警告或错误。要执行参数分配,可以使用if语句抛出未定义的错误,或者可以利用强制参数。

1
2
3
4
5
6
mandatory = () => {
throw new Error('Missing parameter!');
}
foo = (bar = mandatory()) => { // 这里如果不传入参数,就会执行manadatory()函数报出错误
return bar;
}

隐式返回值

返回值是我们通常用来返回函数最终结果的关键字。只有一个语句的箭头函数,可以隐式返回结果(函数必须省略大括号{ },以便省略返回关键字)。

要返回多行语句(例如对象文本),需要使用( )而不是{ }来包裹函数体。这样可以确保代码以单个语句的形式进行求值。

1
2
3
4
5
6
7
function calcCircumference(diameter) {
return Math.PI * diameter
}
// 简写为:
calcCircumference = diameter => (
Math.PI * diameter;
)

惰性载入函数

在某个场景下我们的函数中有判断语句,这个判断依据在整个项目运行期间一般不会变化,所以判断分支在整个项目运行期间只会运行某个特定分支,那么就可以考虑惰性载入函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function foo(){
if(a !== b){
console.log('aaa')
}else{
console.log('bbb')
}
}

// 优化后
function foo(){
if(a != b){
foo = function(){
console.log('aaa')
}
}else{
foo = function(){
console.log('bbb')
}
}
return foo();
}

那么第一次运行之后就会覆写这个方法,下一次再运行的时候就不会执行判断了。当然现在只有一个判断,如果判断很多,分支比较复杂,那么节约的资源还是可观的。

一次性函数

跟上面的惰性载入函数同理,可以在函数体里覆写当前函数,那么可以创建一个一次性的函数,重新赋值之前的代码相当于只运行了一次,适用于运行一些只需要执行一次的初始化代码

1
2
3
4
5
6
7
8
9
var sca = function() {
console.log('msg')
sca = function() {
console.log('foo')
}
}
sca() // msg
sca() // foo
sca() // foo

代码复用

Object [key]

虽然将 foo.bar 写成 foo [‘bar’] 是一种常见的做法,但是这种做法构成了编写可重用代码的基础。许多框架使用了这种方法,比如 element 的表单验证。

请考虑下面这个验证函数的简化示例:

1
2
3
4
5
6
7
8
function validate(values) {
if(!values.first)
return false;
if(!values.last)
return false;
return true;
}
console.log(validate({first:'Bruce',last:'Wayne'})); // true

上面的函数完美的完成验证工作。但是当有很多表单,则需要应用验证,此时会有不同的字段和规则。如果可以构建一个在运行时配置的通用验证函数,会是一个好选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// object validation rules
const schema = {
first: {
required:true
},
last: {
required:true
}
}

// universal validation function
const validate = (schema, values) => {
for(field in schema) {
if(schema[field].required) {
if(!values[field]) {
return false;
}
}
}
return true;
}
console.log(validate(schema, {first:'Bruce'})); // false
console.log(validate(schema, {first:'Bruce',last:'Wayne'})); // true

现在有了这个验证函数,我们就可以在所有窗体中重用,而无需为每个窗体编写自定义验证函数。

数字字面量

不同进制表示法

ES6 中新增了不同进制的书写格式,在后台传参的时候要注意这一点。

1
2
3
4
5
29 // 10 进制
035 // 8 进制 29 原来的方式
0o35 // 8 进制 29 ES6 的方式
0x1d // 16 进制 29
0b11101 // 2 进制 29

精确到指定位数的小数

将数字四舍五入到指定的小数位数。使用 Math.round() 和模板字面量将数字四舍五入为指定的小数位数。 省略第二个参数 decimals ,数字将被四舍五入到一个整数。

1
2
3
const round = (n, decimals = 0) => Number(`${Math.round(`${n}e${decimals}`)}e-${decimals}`)
round(1.345, 2) // 1.35
round(1.345, 1) // 1.3

数字补 0 操作

感谢网友 @JserWang @vczhan 提供 这个小技巧
有时候比如显示时间的时候有时候会需要把一位数字显示成两位,这时候就需要补 0 操作,可以使用slice和 string 的padStart方法

1
2
3
4
5
const addZero1 = (num, len = 2) => (`0${num}`).slice(-len)
const addZero2 = (num, len = 2) => (`${num}`).padStart( len , '0')

addZero1(3) // 03
addZero2(32,4) // 0032

数组

reduce实现map和filter

假设现在有一个数列,你希望更新它的每一项(map 的功能)然后筛选出一部分(filter 的功能)。如果是先使用 map 然后 filter 的话,你需要遍历这个数组两次。
在下面的代码中,我们将数列中的值翻倍,然后挑选出那些大于 50 的数。

1
2
3
4
5
6
7
8
9
const numbers = [10, 20, 30, 40];
const doubledOver50 = numbers.reduce((finalList, num) => {
num = num * 2;
if (num > 50) {
finalList.push(num);
}
return finalList;
}, []);
doubledOver50; // [60, 80]

统计数组中相同项的个数

很多时候,你希望统计数组中重复出现项的个数然后用一个对象表示。那么你可以使用 reduce 方法处理这个数组。

下面的代码将统计每一种车的数目然后把总数用一个对象表示。

1
2
3
4
5
6
var cars = ['BMW','Benz', 'Benz', 'Tesla', 'BMW', 'Toyota'];
var carsObj = cars.reduce(function (obj, name) {
obj[name] = obj[name] ? ++obj[name] : 1;
return obj;
}, {});
carsObj; // => { BMW: 2, Benz: 2, Tesla: 1, Toyota: 1 }

使用解构来交换参数数值

有时候你会将函数返回的多个值放在一个数组里。我们可以使用数组解构来获取其中每一个值。

1
2
3
4
5
let param1 = 1;
let param2 = 2;
[param1, param2] = [param2, param1];
console.log(param1) // 2
console.log(param2) // 1

当然我们关于交换数值有不少其他办法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var temp = a; a = b; b = temp                
b = [a, a = b][0]
a = a + b; b = a - b; a = a - b
```
### 接收函数返回的多个结果
在下面的代码中,我们从 /post 中获取一个帖子,然后在 /comments 中获取相关评论。由于我们使用的是 async/await,函数把返回值放在一个数组中。而我们使用数组解构后就可以把返回值直接赋给相应的变量。
```javascript
async function getFullPost(){
return await Promise.all([
fetch('/post'),
fetch('/comments')
]);
}
const [post, comments] = getFullPost();

将数组平铺到指定深度

使用递归,为每个深度级别 depth 递减 1 。 使用 Array.reduce() 和 Array.concat() 来合并元素或数组。 基本情况下,depth 等于 1 停止递归。 省略第二个参数,depth 只能平铺到 1 (单层平铺) 的深度。

1
2
3
4
5
6
const flatten = (arr, depth = 1) =>
depth != 1
? arr.reduce((a, v) => a.concat(Array.isArray(v) ? flatten(v, depth - 1) : v), [])
: arr.reduce((a, v) => a.concat(v), []);
flatten([1, [2], 3, 4]); // [1, 2, 3, 4]
flatten([1, [2, [3, [4, 5], 6], 7], 8], 2); // [1, 2, 3, [4, 5], 6, 7, 8]

数组的对象解构

数组也可以对象解构,可以方便的获取数组的第 n 个值

1
2
3
4
5
const csvFileLine = '1997,John Doe,US,john@doe.com,New York';
const { 2: country, 4: state } = csvFileLine.split(',');

country // US
state // New Yourk

对象

使用解构删除不必要属性

有时候你不希望保留某些对象属性,也许是因为它们包含敏感信息或仅仅是太大了(just too big)。你可能会枚举整个对象然后删除它们,但实际上只需要简单的将这些无用属性赋值给变量,然后把想要保留的有用部分作为剩余参数就可以了。

下面的代码里,我们希望删除 _internal 和 tooBig 参数。我们可以把它们赋值给 internal 和 tooBig 变量,然后在 cleanObject 中存储剩下的属性以备后用。

1
2
3
let {_internal, tooBig, ...cleanObject} = {el1: '1', _internal: "secret", tooBig: {}, el2: '2', el3: '3'};

console.log(cleanObject); // {el1: '1', el2: '2', el3: '3'}

在函数参数中解构嵌套对象

在下面的代码中,engine 是对象 car 中嵌套的一个对象。如果我们对 engine 的 vin 属性感兴趣,使用解构赋值可以很轻松地得到它。

1
2
3
4
5
6
7
8
9
10
11
12
var car = {
model: 'bmw 2018',
engine: {
v6: true,
turbo: true,
vin: 12345
}
}
const modelAndVIN = ({model, engine: {vin}}) => {
console.log(`model: ${model} vin: ${vin}`);
}
modelAndVIN(car); // => model: bmw 2018 vin: 12345

常用 JS 函数

时间格式化

界面展示的时间千变万化, 所以一个处理时间的函数,它的重要性就不言而喻了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export function formatDate (oldDate, fmt) {  
let date = new Date()
if (typeof oldDate === 'string' || typeof oldDate === 'number') {
date = new Date(+oldDate)
} else {
date = oldDate
}
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length)) }
let o = {
'M+': date.getMonth() + 1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds() }
function padLeftZero (str) {
return ('00' + str).substr(str.length)
}
for (let k in o) {
if (new RegExp(`(${k})`).test(fmt)) {
let str = o[k] + ''
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? str : padLeftZero(str))
}
}
return fmt
}

formatDate 接受俩个参数, oldDate 类型可以是 Date,String,Number。因为现在用时间戳传递时间真的是蛮多,也蛮方便的,而JS 是一门弱类型语言,所以我将 String 和 Number 类型的数据统一当做时间戳来处理。

fmt 是格式化的类型:yyyy-MM-dd hh:mm,其中的 yyyy | MM | dd | hh | mm 是分别匹配 年 | 月 | 日 | 时 | 分 的关键字。其中的连字符是可以随意替换的,只展示年月将其他关键字去掉即可。举几个例子:

  • yyyy年MM月dd -> 2019年09月7日
  • hh分mm秒 -> 16分53秒

以“天”为单位获取响应的时间戳

通常都会获取三天前的时间,12 个以内的数据,24 小时以内的数据,因此我弄了一个以天为单位获取时间戳的函数

1
export function setDate(num) {  return Date.now() + num * 24 * 60 * 60 * 1000}

时间为正可以获得未来的时间,时间为负可以获得过去时间。举个例子:

  • 12 个小时之前的时间 -> setDate(-.5)
  • 24 个小时之前的时间 -> setDate(-1)
  • 三天后的时间 -> setDate(3)

3.获取 URL 中的参数

这个需求在三大框架的时代应用貌似不多了,不过面试中问的还是蛮多的,了解一下是好的

简单实现

1
2
var urlParams = new URLSearchParams('?post=1234&action=edit');
console.log(urlParams.get('action')); // "edit"

看了一下浏览器支持情况还是蛮好的,除了万恶的 ie复杂实现function

1
2
3
4
5
6
7
8
9
10
11
getUrlParams(param){
// 有赖于浏览器环境, window.location.search 是浏览器函数
// 意思是:设置或返回从问号 (?) 开始的 URL(查询部分)。
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if(pair[0] == param){return pair[1];}
}
return(false);
}

举个例子: http://xuyuechao.top?a=3&b=5&c=8888

  • getUrlParams(‘a’) -> 3
  • getUrlParams(‘b’) -> 5
  • getUrlParams(‘c’) -> 8888

手机端判断浏览器类型

1
2
3
4
5
6
7
8
BrowserInfo = {      
isAndroid: Boolean(navigator.userAgent.match(/android/ig)),
isIphone: Boolean(navigator.userAgent.match(/iphone|ipod/ig)),
isIpad: Boolean(navigator.userAgent.match(/ipad/ig)),
isWeixin: Boolean(navigator.userAgent.match(/MicroMessenger/ig)),
isAli: Boolean(navigator.userAgent.match(/AlipayClient/ig)),
isPhone: Boolean(/(iPhone|iPad|iPod|iOS|Android)/i.test(navigator.userAgent))
}

目前主要支持 安卓 & 苹果 & ipad & 微信 & 支付宝 & 是否是手机端。

数组降维

二维数组

1
2
let arr = [ [1], [2], [3] ]
arr = Array.prototype.concat.apply([], arr); // [1, 2, 3]

多维数组降维

1
2
let arr = [1, 2, [3], [[4]]]
arr = arr.flat(3) // [1, 2, 3, 4]

flat 有兼容性问题,手机端问题不大。浏览器端不兼容 edge。填 Infinity 可展开任意深度的数组

深拷贝

使用变量 a 拷贝对象 b,改变 a 中的值 b 中的值也会跟着改变,这叫做浅拷贝。要想让 a 独立于 b 就需要深拷贝
简易处理

1
2
3
function deepClone() {
return JSON.parse(JSON.stringify(obj))
}

既然是简易处理就有他的不足,上面主要是用了 JSON 的序列化和反序列化。而 JSON 是不支持函数和 undefined 的因此碰到这些情况会缺失,但是已经能够满足大部分情况了复杂处理
复杂处理就需要采用递归的方式了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function deepClone(obj) {  
function isClass(o) {
if (o === null) return "Null";
if (o === undefined) return "Undefined";
return Object.prototype.toString.call(o).slice(8, -1);
}
var result;
var oClass = isClass(obj);
if (oClass === "Object") {
result = {};
} else if (oClass === "Array") {
result = [];
} else {
return obj;
}
for (var key in obj) {
var copy = obj[key];
if (isClass(copy) == "Object") {
result[key] = arguments.callee(copy);//递归调用
} else if (isClass(copy) == "Array") {
result[key] = arguments.callee(copy);
} else {
result[key] = obj[key];
}
}
return result;
}

防抖 & 节流

防抖和节流属于高阶技巧,业务中比较多见的场合也就是搜索内容改变提示信息。即使不加也也不一定能看出区别,但是加了新手维护代码可能会崇拜你哦。
防抖

1
2
3
4
5
6
7
8
9
10
11
12
13
function debounce(func, wait) {
let timeout;
return function () {
let context = this;
let args = arguments;

if (timeout) clearTimeout(timeout);

timeout = setTimeout(() => {
func.apply(context, args)
}, wait);
}
}

节流

1
2
3
4
5
6
7
8
9
10
11
12
function throttle(func, wait) {
let previous = 0;
return function() {
let now = Date.now();
let context = this;
let args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}

获取数组极值

1
2
3
4
5
6
7
8
function smallest(array){                           
return Math.min.apply(Math, array);
}
function largest(array){
return Math.max.apply(Math, array);
}
smallest([0, 1, 2.2, 3.3]); // 0
largest([0, 1, 2.2, 3.3]); // 3.3

用 es6 的实现方式

1
2
3
let list = [1, 2, 3, 4, 5]
Math.max(...list) // 5
Math.min(...list) // 1

判断小数是否相等

1
2
3
4
5
6
function epsEqu(x,y) {  
return Math.abs(x - y) < Math.pow(2, -52);
}
// 举例
0.1 + 0.2 === 0.3 // false
epsEqu(0.1 + 0.2, 0.3) // true

Number.EPSILON ,Number.EPSILON === Math.pow(2, -52)
因此上述方法也可以这么写

1
2
3
function epsEqu(x,y) {  
return Math.abs(x - y) < Number.EPSILON;
}

用户输入的是十进制数字,计算机保存的是二进制数。所以计算结果会有偏差,所以我们不应该直接比较非整数,而是取其上限,把误差计算进去。这样一个上限称为 machine epsilon,双精度的标准 epsilon 值是 2^-53 (Math.pow(2, -53))

参考资料

学会这些 JS 小技巧,提升编码幸福度
收下这波 JS 技巧,从此少加班

注:本文不以盈利为目的,仅做学习交流使用,若有侵权,请联系我删除,万分感谢!

JS 谜一样的this(浏览器中)

JavaScript 中的this是根据运行时的环境所决定的

①this在全局作用域中

1
2
3
4
5
6
foo = 'abc'
console.log(foo) // abc

var foo = 'def'
console.log(this.foo) // def
// 这里的this指的是全局对象window(浏览器中)
  • 在浏览器中,指向window对象

②this在函数/对象方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var boat = {
size: 'normal',
boatInfo: function() {
console.log(this === boat, this.size)
}
}

boat.boatInfo() // true, 'normal'

var bigBoat = {
size: 'big'
}

bigBoat.boatInfo = boat.boatInfo
bigBoat.boatInfo() // false, 'big'

首先要明白,在任何函数中,this的指向都不是静态的(static)。它总是在你调用一个函数,但尚未执行函数内部代码前被指定。实际上,this是 被调用的函数的父作用域提供的。

this,指向调用它所属的那个函数的那个对象

  • 如果函数左边是一个引用,那么函数里的this指向的就是这个引用
  • 否则this指向的就是全局对象(浏览器 -> window)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function bar() {
console.log(this)
}
bar()
// 这里,this指向的是全局对象。我们先看函数的左边,没东西,
// 那么bar属于谁呢?bar属于全局对象,所以this指向的就是全局对象。

var foo = {
baz: function() {
console.log(this)
}
}
foo.baz()
// 这里,this指向的是foo,先看函数左边是foo,所以baz里的this指向的就是foo

由于js中Object和Function是引用类型,所以在引用发生变化的时候,this的指向也会发生改变

1
2
3
4
5
6
7
8
9
10
var foo = {
baz: function() {
console.log(this)
}
}
foo.baz()
// 这里的this指向foo
var anotherBaz = foo.baz
anotherBaz()
// this指向全局对象,在浏览器中这个函数相当于window.anotherBaz()

嵌套在对象里的this的指向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var anum = 0

var foo = {
anum: 10,
baz: {
anum: 20,
bar: function() {
console.log(this.anum)
}
}
}
foo.baz.bar() // 20
// 函数左边是baz,所以this就是foo.baz,
// this.anum = foo.baz.anum

var hello = foo.baz.bar;
hello() // 0
// 函数左边是啥也没有,所以this指向全局对象
// this.anum = window.anum

值得注意的是下面这几种用法,都会改变this的指向。

1
2
3
4
5
6
7
8
9
10
11
12
13
var obj ={
foo: function () {
console.log(this)
}
}

obj.foo() // obj
// 情况一
;(obj.foo = obj.foo)() // window
// 情况二
;(false || obj.foo)() // window
// 情况三
;(1, obj.foo)() // window

上面代码中,obj.foo就是一个值。这个值真正调用的时候,运行环境已经不是obj了,而是全局环境,所以this不再指向obj

  • 可以这样理解,JavaScript 引擎内部,obj和obj.foo储存在两个内存地址,称为地址一和地址二。
  • obj.foo()这样调用时,是从地址一调用地址二,因此地址二的运行环境是地址一,this指向obj。
  • 但是,上面三种情况,都是直接取出地址二进行调用,这样的话,运行环境就是全局环境,因此this指向全局环境。

上面三种情况等同于下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 情况一
(obj.foo = function () {
console.log(this)
})()
// 等同于
(function () {
console.log(this)
})()

// 情况二
(false || function () {
console.log(this)
})()

// 情况三
(1, function () {
console.log(this)
})()

如果this所在的方法不在对象的第一层,这时this只是指向当前一层的对象,而不会继承更上面的层。

1
2
3
4
5
6
7
8
9
10
var obj = {
name: 'Hello',
son: {
foo: function() {
console.log(this.name)
}
}
}

obj.son.foo() // undefined

此时的this指向的son,son没有name属性,就当然undefined了

③this在构造函数和类中

构造函数中的this,指的是实例对象。

1
2
3
4
5
6
7
8
9
10
11
var Obj = function (x) {
this.x = x
function sayHi() {
console.log(this.x)
}
}
Obj.prototype.sayHi = function () {
console.log(this.x)
}
var obj1 = new Obj('Hello World!')
obj1.sayHi() // "Hello World!"

类中的this,指的是实例对象。

1
2
3
4
5
6
7
8
9
10
class Obj {
constructor(x) {
this.x = x;
}
sayHi() {
console.log(this.x)
}
}
var obj1 = new Obj('Hello World!')
obj1.sayHi() // "Hello World!"

④this在dom事件中

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="test">今天是个好日子</div>
<script>
var myElem = document.getElementById('test')
myElem.onclick = function() {
console.log(this.innerHTML)
}

myElem.onclick() // 今天是个好日子
</script>
😑😶😏😣😥😮😯😍😘😗😙😚
<button onclick="this.style.display='none'">
点我后我就消失了
</button>

参考资料

阮一峰 JavaScript入门教程

日暮乡关 有道云笔记

菜鸟教程 Javascript教程

注:本文不以盈利为目的,仅做学习交流使用,若有侵权,请联系我删除,万分感谢!

JS Array对象的reduce方法

reduce的用法

array.prototype.reduce()

reduce 方法接受2个参数,一个回调函数,一个可选的自定义值

  • 回调函数

    执行数组中每个值的函数,包含四个参数:

    • 累积变量,默认为数组的第一个成员

      或’自定义累积变量’(见于下方)

    • 当前变量,默认为数组的第二个成员

      数组中正在处理的元素

    • 当前位置 可选

      数组中正在处理的当前元素的索引。 如果提供了initialValue,则起始索引号为0,否则为1

    • 原数组 可选

      调用reduce()的原数组

  • 自定义累积变量 可选

    作为第一次调用 回调函数 时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错。

    注意:自定义累积变量 不是替换掉数组的第一个元素!

reduce的使用

数组求和

1
[1, 3, 5, 7, 9].reduce((x, y) => x + y) // 25

数组求积

1
[2, 3, 5].reduce((x, y) => x * y) // 30

[1, 3, 5, 7, 9]变换成整数13579

1
[1, 3, 5, 7, 9].reduce((x, y) => x * 10 + y) // 13579

把一个字符串'13579'先变成Array [1, 3, 5, 7, 9],再利用reduce() 写出一个把字符串转换为Number的函数

不要使用JavaScript内置的parseInt()函数,利用map和reduce操作实现一个string2int()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
'use strict';

function string2int(s) {
return s.split('').map(ele => ele * 1).reduce((x, y) => {
return x * 10 + y
})
}

// function string2int(s) {
// return s.split(',').reduce((x, y) => y - 0, 0)
// }

// 测试:
if (string2int('0') === 0 && string2int('12345') === 12345 && string2int('12300') === 12300) {
if (string2int.toString().indexOf('parseInt') !== -1) {
console.log('请勿使用parseInt()!')
} else if (string2int.toString().indexOf('Number') !== -1) {
console.log('请勿使用Number()!')
} else {
console.log('测试通过!')
}
}
else {
console.log('测试失败!')
}

reduceRight的用法

array.prototype.reduceRight()

reduce方法和reduceRight方法依次处理数组的每个成员,最终累计为一个值。它们的差别是,reduce是从左到右处理(从第一个成员到最后一个成员),reduceRight则是从右到左(从最后一个成员到第一个成员),其他完全一样。

1
2
3
4
5
6
function subtract(prev, cur) {
return prev - cur
}

[3, 2, 1].reduce(subtract) // 0
[3, 2, 1].reduceRight(subtract) // -4

由于这两个方法会遍历数组,所以实际上还可以用来做一些遍历相关的操作。比如,找出字符长度最长的数组成员。

1
2
3
4
5
6
7
function findLongest(entries) {
return entries.reduce(function (longest, entry) {
return entry.length > longest.length ? entry : longest;
}, '');
}

findLongest(['aaa', 'bb', 'c']) // "aaa"

上面代码中,reduce的参数函数会将字符长度较长的那个数组成员,作为累积值。这导致遍历所有成员之后,累积值就是字符长度最长的那个成员。

参考资料

廖雪峰-JavaScript教程

阮一峰-JavaScript教程

MDN-火狐开发者文档

注:本文不以盈利为目的,仅做学习交流使用,若有侵权,请联系我删除,万分感谢!