yancy

yancy 查看完整档案

北京编辑  |  填写毕业院校字节跳动  |  前端 编辑填写个人主网站
编辑

你若盛开,清风自来

个人动态

yancy 发布了文章 · 4月8日

寻找一定范围内的质数.md

寻找一定范围内的质数

今天给大家分享一个比较简单,但是比较好玩的题目 -- 寻找一定范围内的质数

第一眼看到这个题目大家一定会这么想:这么简单的题目,有必要给大家分享吗?

还请大家带着这样的疑问,继续深入的向下看。

1. 第一版:双循环

看到这样的题目,大家肯定会第一时间想起来双循环的解决方式,不忙,先来个小栗子看下

栗子:🌰

function findPrimeNumbers (n) {
  const result = []
  for (let i = 4; i < n; i++) {
    let status = false
    for (let j = 2; j < i; j++) {
      if (i % j === 0) {
        status = true
        break
      }
    }
    if (!status) {
      result.push(i)
    }
  }
  return result
}

OK,题目写完了,我们就来验证一下。

1. 查找 100 以内的质数数量。
findPrimeNumbers1(100) 
/* [
   5,  7, 11, 13, 17, 19, 23, 29,
  31, 37, 41, 43, 47, 53, 59, 61,
  67, 71, 73, 79, 83, 89, 97
]
*/

这里我们可以看到,逻辑没问题,能正常筛选出来。

2. 查找 10000 以内的质数
findPrimeNumbers1(10000)
/*
[
    5,   7,  11,  13,  17,  19,  23,  29,  31,  37,  41,  43,
   47,  53,  59,  61,  67,  71,  73,  79,  83,  89,  97, 101,
  103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163,
  167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229,
  233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293,
  307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373,
  379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443,
  449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521,
  523, 541, 547, 557,
  ... 1127 more items
]
*/

这个也没什么问题,也可以查找出来

3. 查找 1000000 以内的问题
findPrimeNumbers1(1000000)

这里,当我们运行程序的时候会发现一个问题,很长很长时间都没有将程序执行完成。

所以这个程序会造成超时错误。

2. 第二版:优化的双循环

到这里有的同学会说,如果我判断,当前是偶数就不做校验,直接跳过。会不会可以提高性能?

OK,我们就来验证下

栗子:🌰

function findPrimeNumbers (n) {
  const result = []
  for (let i = 4; i < n; i++) {
    let status = false
    if (i % 2 === 0) continue // 添加了这一行代码
    for (let j = 2; j < i; j++) {
      if (i % j === 0) {
        status = true
        break
      }
    }
    if (!status) {
      result.push(i)
    }
  }
  return result
}

经过上述三个数字100、10000、1000000 的验证,可以发现一个问题,1000000 以内的数量我们依旧查找不出来。

3. 第三版:再次优化查询

到这一步,可能很多同学都已经不知所措了,既然查找不到 1000000 内的质数,那就不查了呗。何必为难自己。

不错不错,深深的感悟到了 -- 退一步海阔天空 这一道理。

不过嘛,有问题总是要解决的。万一哪天用到了呢。

好了,不多说,我们来看下第三版的实现。

栗子:🌰

function findPrimeNumbers (n) {
  const arr = new Array(n).fill(1)
  for (let i = 2; i < n; i++) {
    if (arr[i]) {
      for (let j = 2; i * j < n; j++) {
        arr[i * j] = 0
      }
    }
  }

  let result = []

  for (let i = 0, len = arr.length; i < len; i++) {
    if (arr[i]) {
      result.push(i)
    }
  }

  return result
}

经过 100、10000、1000000 三个数字的验证,我们发现已经可以查找到 1000000 内的质数了。

现在我们来解释一下实现。

解释:

  • 首先,创建一个长度为 n 的数组,并使用 1 填充。
  • 遍历这个数组,如果当前值为 1 ,则将当前值所有的倍数修改为 0
  • 遍历完成后,查找数组中值为 1 的索引。添加到结果中并返回。

此题很简单,也很好理解,但是确实挺好玩,所以跟大家分享下。

今天的内容就分享到这儿咯

Bye~

查看原文

赞 0 收藏 1 评论 0

yancy 发布了文章 · 4月2日

翻转单链表

反转单链表

今天跟大家分享一个已经被写烂了的题目 --> 反转单链表

相信大家一定在日常的工作、面试中被这道题所难倒过,而且也相信很多人都会在网上查找实现方案,并且会牢牢记住。

但是!每次觉得自己记的很好了,一到写代码的时候还是两眼一抹黑。

这就是知其然不知其所以然的后果,一定要深入的弄懂一个题目,并不要只记住代码,世上代码千千万,又能记住多少呢?

接下来,我们就来看下如何实现一个单链表的反转。

1. 创建一个单链表

首先我们需要一个单链表。(相信单链表的定义一定不需要多说,大家都知道)

上栗子:🌰

const linkList = {
  val: 1,
  next: {
    val: 2,
    next: {
      val: 3,
      next: {
        val: 4,
        next: {
          val: 5,
          next: {
            val: 6,
            next: null
          }
        }
      }
    }
  }
}

首先映入眼帘的是一长串嵌套的数据,嗯,乍一看有点儿眼花。

请原谅作者的偷懒,但是这也是最容易让人理解的方式。

2. 单链表反转

数据定义好之后,我们就需要来将此链表做反转。

看好了,接下来的代码正是让大家疑惑的点。

🌰:

function reverseLinkList (linkList) {
  if (!linkList) return null

  let result = null
  let preLinkList = linkList
  let nextLinkList = linkList.next

  while(preLinkList) {
    preLinkList.next = result
    result = preLinkList
    preLinkList = nextLinkList

    if(nextLinkList) {
      nextLinkList = nextLinkList.next
    }
  }
  return result
}

仔细一看,司高义!满屏惊叹。

然后~~~,这是写了个什么玩意儿,看不懂。

莫慌,这都是大家疑惑的点,我们来详细解释一下。

题目解析:
1. 变量解析
let result = null
let preLinkList = linkList
let nextLinkList = linkList.next

这里定义了三个变量

  • result 是获取到的反转后的结果
  • preLinkList是当前操作的链表
  • nextLinkList 保存的是需要遍历的链表
2. 循环解析
while(preLinkList) {
  preLinkList.next = result
  result = preLinkList
  preLinkList = nextLinkList

  if(nextLinkList) {
    nextLinkList = nextLinkList.next
  }
}

这个循环也是大家最疑惑的地方。给大家详细说明一下。接下来我们看下执行步骤。

  • 每次执行都先将 preLinkList 变形为我们想要的数据。它的变化规则如下。
  • 之后将此数据赋值在 result 上。
  • 然后 preLinkList 递进,递进的条件就是将 nextLinkList 赋值给 preLinkList

接下来我们看下整体的执行流程图解。

image

以上就是反转单链表的全部流程了。

今天的分享我们到这里也就结束了,希望可以让你有所收获,Bye~

查看原文

赞 0 收藏 0 评论 0

yancy 发布了文章 · 3月10日

求字符串的全排列

今天给大家分享一道简单的算法题 -- 求字符串的全排列

相信很多人都会被这道题考查过,也有很多人不能写出来,放轻松,接下来我们来看下如何实现这道题。

首先我们来看下问题是什么。

给定一个字符串,求出这个字符串所有可能出现的排列组合。

如: abc

输出:

[ 'cba', 'bca', 'cab', 'acb', 'bac', 'abc' ]


准备好了吗? 来一起看下如何实现吧。

解答思路:

首先我们来做个假设:

  1. 当前给的字符串为 'a' 。那么返回的只有一个排列 [ 'a' ]
  2. 当前给的字符串为 'ab' 。则返回 [ 'ab', 'ba' ]
  3. 当前给的字符串为 'abc' 。则返回 [ 'cba', 'bca', 'cab', 'acb', 'bac', 'abc' ]
  4. ………………

1. 一个字符的情况

乍一看好像没什么规律,不急,我们先来实现以下只有一个字符的情况

栗子:🌰

const str = 'a'

function fullPer (str) {
  return [str]
}

fullPer(str) // [ 'a' ]

相信这个代码大家都会写。接下来,难度继续深入。

2. 两个字符的情况

第二步,我们需要添加两个字符的情况。两个的情况下,为了便于我们理解,我们使用递归的方式实现。

使用递归我们需要先找到以下几个条件

  • 何时结束:str 的长度为 1 的时候
  • 如何递进:每次留一个字符,然后与剩下的字符相加
  • 返回结果:返回一个数组

栗子:🌰

const str = 'ab'

function fullPer(str) {
  if (str.length <= 1) {
    return [str]
  }
  
  let result = [] // 定义一个结果集,用来收集所有的排列
  for(let i = 0,len = str.length;i < len; i ++) {
    let child = str[i] // 保留当前字符
    let last = str.replace(child, '') // 获得剩下的所有字符
    
    result.push(fullPer(last)[0] + child) // 将得到的下一个字符与当前字符做拼接
  }
  
     return result // 将得到的结果返回
}

fullPer(str) // [ 'ba', 'ab' ]

解释一下:

两个字符的情况下,我们写的代码有点儿多,所以不好理解,那我们就来解释一下代码。

  1. 代码中,如果我们当前保留的字符是 a 的话,那么下一次传入的就是 b 。然后字符长度为1,不做处理,直接返回一个数组。之后我们将得到的字符b保留字符a相加,得到 ba
  2. 同样的道理,保留字符为 b ,下次传入 a,得到 ab
  3. 将两次的结果分别添加到结果集中,返回得到 [ 'ba', 'ab' ]

3. 三个字符的情况

三个字符与两个字符相差不大,只不过我们需要变动得到字符之后的处理。我们来看下代码

栗子:🌰

const str = 'abc'

function fullPer(str) {
  if (str.length <= 1) {
    return [str]
  }
  
  let result = []
  for(let i = 0,len = str.length;i < len; i ++) {
    let child = str[i]
    let last = str.replace(child, '')
    // 唯一与第二步不一样的地方
    const middle = fullPer(last).map(item => item + child)
    // 因为这里得到的是一个数组,所以我们需要将result与middle做拼接
    result = result.concat(middle)
  }
  
     return result
}

fullPer(str) // [ 'cba', 'bca', 'cab', 'acb', 'bac', 'abc' ]

解释一下:

  1. 三个字符的情况,我们得到的是一个多元素的数组,所以只能通过遍历的方式都添加上之前所保留的字符child
  2. 结果集也不能使用push,得使用 concat 将两个数组做拼接。

4. 三个字符以上的情况

我们先验证一下其他数量字符的情况下,上边的函数能不能用,这里我们就输出结果的总数,不输出具体值了。

// 四个字符
const str = 'abcd'
fullPer(str).length // 24

// 五个字符
const str = 'abcd3'
fullPer(str).length // 120

根据阶乘公式可知 ---> 我们算对了。有时候惊喜就是这么突然。我们发现解决了三个字符之后就解决了其他排列问题。

OK,今天的分享就到这里了。

Bye ~

查看原文

赞 0 收藏 0 评论 0

yancy 发布了文章 · 3月1日

偷偷运行的逻辑 - JavaScript隐式类型转换

将写作当成兴趣,并一直进行下去。曾经这是个小小的奢望,现在已经在逐步的实现中。

长话短说,既然是技术文,就不发这么多感慨了,接下来,一起进入今天的正题吧。

今天给大家分享的是 JavaScript 中的隐式类型转换问题。相信很多的小伙伴都曾被它困扰过,不论是开发中还是面试过程中。期望今天的分享能给你带来不一样的理解。也能让你之后不再为此烦恼。

1. 基本数据类型

我们都知道,在 javascript 中一共有 7 中数据类型。分别是 Object, Null, Undefined, String, Number, Boolean, Symbol。这些东西我们在平时的开发过程中每天无时无刻不在接触,这里我们不多赘述。

2. 强制类型转换

平时的使用过程中,我们会遇到很多数据类型不一致的问题,同样,强制将不同的数据类型转换为相同的数据类型也是很正常的操作;接下来,我们看看都有什么样的强制转换方法:

2.1 字符串 -> 数字

1. parseInt
parseInt('123') // 123
parseInt('123abc') // 123
parseInt('12.3abc') // 12
parseInt('abc123') // NaN

解释:

此方法只转换 以数字开头的,直到不是数字,然后转换结束,如果字符串不是以数字开头,则转换为 NaN

2. parseFloat
parseFloat('123') // 123
parseFloat('12.3abc') // 12.3
parseFloat('1.2.3abc') // 1.2
parseFloat('abc1.23') // NaN

解释:

规则同 parseInt ,只是注意小数点的转换,只能转换一个小数点,如果是多个小数点,则只保留一个。

3. Number
Number('123') // 123
Number('12.3abc') // NaN
Number('1.2.3abc') // NaN
Number('abc1.23') // NaN

解释:

此方法只转换 全数字 的字符串,如果字符串不是全数字。如'12.3abc','1.2.3abc','abc1.23'………… 的情况,统一转换为 NaN

4. 位运算

位运算的使用在前端是特别少的,以至于很多前端人员不清楚具体使用。下面我们来看看,如何使用位运算将字符串转换为数字

主要有以下几种操作方式可做转换

  • ~
  • << 左移
  • >> 右移

对于位运算的实际运算方式我们暂不做描述,本次只看如何使用它们将字符串转为数字

~~'123'    // 123 (这里是两个 ~~ 波浪线)
'123' << 0 // 123
'123' >> 0 // 123

解释:

使用方法跟 Number 相同。都是只能转换全数字的字符串。不为全数字的字符串,~转换为 NaN ,另外两种转换为 0

2.2 数字 -> 字符串

1. 使用 + 运算符。
'' + 123 // '123'
2. 使用 toString 方法
let num = 123
num.toString() // ‘123’
3. 使用 String 方法
let num = 123
String(num) // '123'

2.3 转换为布尔值

1. !! 方法。(双重否定即为肯定。使用双重非可以得到原始值转换的布尔值)
let num = 123
!!num // true

let str = '123'
!!str // true
2. Boolean 方法
let num = 123
let str = '123'
Boolean(123) // true
Boolean('123') // true

注意:

JavaScript 中为 false 的情况:

  • '',"" 空字符串
  • 0 数字 0
  • undefined
  • null
  • NaN
  • false

3. 隐式类型转换

3.1 可触发隐式类型转换的操作

  • 四则运算 +, -, *, /
  • 比较 > < >= <= ==

    • 注意: === 是不会触发隐式类型转换的。
  • 判断 if, while

3.2 toStringvalueOf 说明

此两种方法是将复杂数据类型转换为原始值输出。

1. 调用 valueOf 方法后
  • String, Number, Boolean 返回的分别是 字符串值,数字值,布尔值。
  • Object, Array, Function 返回的是自身
  • Date 返回的是从开始到现在的毫秒值
2. 调用 toString 方法后
  • String, Number, Boolean 返回的本别是字符串类型的值
  • Object 返回的是 [object Object]
  • Array 返回的是 空字符串。因为在 Array 中重写了这个方法
  • function 返回的是函数本身的字符串
  • Date 返回的是时间,并非毫秒数

注意:

在获取原始值(toPrimitive)时,会先调用 valueOf 方法,如果返回的不是原始值(也就是说返回的不是基本数据类型),则会继续调用 toString 方法。如果还不是原始值。则会报错。

3.3 具体实例解析

请在查看解析之前尝试解答下方问题

// 数组
[] == ![]   // 1
[] == []    // 2
[] == false // 3
[] == true  // 4
[1] == 1    // 5
[] == 0     // 6
[12] < [13] // 7

// 对象
{} == {}    // 8
{} == !{}   // 9
{} != {}    // 10

// 结合版
[] + {}     // 11
{} + []     // 12
{} + {}     // 13
[] + []     // 14
{} + 1      // 15
1 + {}      // 16

答案来咯,准备好了没

1. [] == ![] 执行步骤
// 将原题中的 ![] 转换为原始值 --> ![] 为false
[] == false
// 将 [] 转换为原始值 [].valueOf() 返回自身,继续调用 toString 返回 空字符串
'' == false
// 将空字符串转换为 布尔值,空字符串为false
false == false
// 得到结果为 [] == ![] --> true
2. [] == []
比较的是地址,两个数组的地址不相同。结果为false
3. [] == false

解答步骤同第一题[] == ![]

4. [] == true

解答步骤同第一题[] == ![]

5. [1] == 1
// 将 [1] 获取原始值, 调用 valueOf 返回自身,继续调用 toString 返回 '1'
1 == 1
// 得到结果 [1] == 1 --> true
6. [] == 0
// 将 [] 获取原始值, 调用 valueOf 返回自身,继续调用 toString 返回 ‘’
‘’ == 0
// 将空字符串转换为数字 '' --> 0
0 == 0
// 得到结果 [] == 0 --> true
7. [12] < [13]
// 将左右都转换为原始值
'12' < '13'
// 得到结果 true
8. {} == {}

[] == []

9. {} == !{}

[] == ![] 不同的是,这里的 {} 转化为字符串之后为[object Object] 。 所以结果与 [] == ![] 相反

10. {} !== {}

{} == {}反结果

11. [] + {}
// 将左右同时获取原始值。
'' + '[object Object]' = '[object Object]'
12. {} + []

如果右边的值不值一个字典格式,则会将大括号当成一个空块儿处理。也就是说此时的表达式可以被转换成如下表达式+ [],然后将 [] 先转为空串,然后转换为数字,得到数字0

所以: {} + [] == 0

13. {} + {}

左右两边都做对象处理,获取原始值

'[object Object]' + '[object Object]' = '[object Object][object Object]'
14. [] + []

获取左右的原始值,都转换为了空字符串。然后做字符串拼接

'' + '' = ''
15. {} + 1

{} + [] ,可变形为 + 1

16. 1 + {}

将右方的 {} 获取原始值,得到 '[object Object]' ,原式可变形为

1 + '[object Object]' = '1[object Object]'

4. 补充点

  • NaN 与任何值都不相等,包括它本身(这得多很,自己跟自己都不相等)
  • undefined 参与的任意一个四则运算,结果都为 NaN
  • 布尔值 true 转数字时,转为 1false 转为数字时为 0
  • 字符串之间比较大小,实际比较的是字符编码。如:a > A = 97 > 65 = true

好了,今天的文章就分享到这儿咯,并没有写太多的概念,只是让大家来多看下实际运行的结果。一通则百通,知晓了实际运行的过程,再遇到相似的问题就游刃有余了。

查看原文

赞 1 收藏 0 评论 0

yancy 发布了文章 · 2月24日

JS中的第二世界--正则表达式

近期的繁忙让我一直没有空闲静下心来好好写一些文章。好在,所有的忙碌都已过去,愿明天更美好。刚过完七夕,带着欢乐的心情写下这篇文章。希望读者能够喜欢哟~~

你是不是经常遇到正则,却不知它在说什么,你是不是就算再项目中看到了,也会选择一眼略过,你是不是整天忘记搜索什么,却不知道有的编辑器搜索支持正则的模糊搜索……

熟悉的旋律萦绕在耳边,却早已不是当初的少年。

工作了很久之后,猛然发现之前自己忽略的正则是这么重要。搜索、查询、命令行、校验、截取、替换…………哪样哪样都离不开这个小东西。很多人不知道的是,正则在 JavaScript 里仿佛一个第二世界一样。看起来简单,实则有很多让我们摸不着头脑的操作。

接下来,让我们顺着本文的介绍,一点儿一点儿深入了解正则表达式,知晓正则的强大能力!

1. 创建正则表达式

JavaScript 中创建正则表达式有两种方式

第一种:构造函数创建

const reg = new Reg('正则表达式')

第二种:字面量创建

const reg = /正则表达式/

2. 使用正则表达式匹配内容

正则匹配分为 精准匹配模糊匹配

精准匹配

只需要我们将需要匹配的内容放入到正则表达式中匹配即可。上手简单

const reg = /正则表达式/
const str = '这是一篇写正则表达式是文章'

str.match(reg) // 正则表达式

这里的正则表达式会精准的搜索到我们要搜索的内容。

模糊匹配

模糊匹配需要用到第三点所描述的内容,这里我们先看下

const reg = /[\u4e00-\u9fa5]+/
const str = '这是一篇写正则表达式Regpx的内容'

str.match(reg) // 这是一篇写正则表达式

这里的正则表达式会根据我们传入的规则来匹配要搜索的内容。注意:这里的\u4e00-\u9fa5表示的是中文范围

\u4e00 表示的是字符集中第一个中文汉字

\u9fa5 表示的是字符集中最后一个中文汉字

ok,知道了怎么创建和使用正则,接下来我们来真正了解一下他吧。

3. 正则表达式介绍

通晓元字符的同学可跳过此小节,直奔第四小节查看。主要内容有 . | \ * + ? () [] {} ^ $ 几种字符使用方法,含义和注意事项,内置匹配项,贪婪和惰性匹配

首先介绍一下前瞻条件,正则可以根据自己的设置来进行多行和全局,局部,忽略大小写等匹配模式,分为:

  • m :多行匹配
  • g: 全局匹配
  • i: 忽略大小写

接下来的内容,就可以看到我们来使用它

3.1 元字符

元字符是正则表达式中最基础的部分,它代表了我们可以查询的内容。come on baby。让我们来认识一下他们吧。

  • . :匹配除去 \n 之外的所有内容
const reg = /.*/
const str1 = 'abc'

// 在str2中添加 \n 换行符
const str2 = 'a\nb\nc'

str1.match(reg) // abc
str2.match(reg) // a
  • |:在正则中进行逻辑判断,(说的比较抽象,我们来看下例子)
const reg = /正则|表达/
const str1 = '正则表达式'
const str2 = '表达式'

str1.match(reg) // 正则
str2.match(reg) // 表达

解释:

如果可以匹配到 | 前边的内容,则匹配前边的。

匹配不到前边的,但是可以匹配后边的内容,则匹配后边的

  • 反斜杠 \ :表示转义
const reg = /\./
const str = 'a.b.c.d'

str.match(reg) // .

可以匹配除 \n 外任意字符的点 . 在使用 \ 转义之后只能匹配到 . 这个字符

需要注意的点:

在有的编程语言中,匹配 \\ 需要四个反斜杠 -- \\\\。但是在 javascript 中,只需要两个。

const reg = /\\/
const str = '\\'

str.match(reg) // \\
  • 星号 *:需要匹配的内容出现连续零次或多次
const reg = /a*/
const str1 = 'aaabbcccddd'
const str2 = 'bbcccddd'
reg.test(str1) // true
reg.test(str2) // true
  • 加号 +:需要匹配的内容出现连续一次或多次
const reg = /a+/
const str1 = 'aaabbcccddd'
const str2 = 'bbcccddd'
reg.test(str1) // true
reg.test(str2) // false
  • 问号 ?:需要匹配的内容出现连续零次或一次
const reg = /a?/
const str1 = 'aaabbcccddd'
const str2 = 'bbcccddd'

reg.test(str1) // true
reg.test(str2) // true
  • 小括号(): 表示组,小括号中的内容表示一组,整体进行匹配。
const reg = /(abc)/
const str1 = 'abab'
const str2 = 'abcab'
reg.test(str1) // false
reg.test(str2) // true

拓展:

使用小括号包裹的匹配内容,可在后续的正则中通过\ + 序号的方式继续调用。有几个小括号就有几个序号。序号从1开始

const reg = /(abc)\1/  // 相当于 const reg = '(abc)abc'
const str = 'abcabc'

str.match(reg)

//   /(a)(b)(c)\1\2/ ---> /(a)(b)(c)ab/
  • 中括号 []:表示范围,括号中的内容只要有一个匹配到就可以
const reg = /[abc]/
const str = "efg"
const str1 = 'aef'

reg.test(str) // false
reg.test(str1) // true

注意:

中括号中可以使用 - 来表示连续的范围

/[0-9]/   0123456789
/[a-z]/   全部英文小写
/[A-Z]/   全部英文大写
  • 大括号 {m,n}:表示出现次数

    m 表示最少出现多少次 n表示最多出现多少次,n可省略,表示无限次

const reg = /a{1,4}/ // a最少出现1次,最多出现4次
const str = 'aaaaa'

reg.test(str) // true

拓展:

{0,} 相当于 *

{1,} 相当于 +

{0,1}相当于 ?

  • 开头^:只匹配开头内容
const reg = /a^/
const str = 'abc'
const str1 = 'bac'

reg.test(str) // true
reg.test(str1) // false

注意:

此符号用在 [] 中,表示取反操作

const reg = /[^abc]/g 
const str = '123'
str.match(reg) // ['1', '2', '3']

这里只能取到不是 abc 的内容

  • 结尾$: 只匹配结尾
const reg = /a$/
const str = 'abc'
const str1 = 'cba'

reg.test(str) // false
reg.test(str1) // true

3.2 内置匹配符

\d:数字

\D:非数字

\s:空格

\S:非空格

\w:数字字母下划线

\W:非数字字母下划线

\b:字符边界

\B:非字符边界

const str = 'hello word! 520'

console.log(str.replace(/\d/g, '*')) // hello word! ***
console.log(str.replace(/\D/g, '*')) // ************520
console.log(str.replace(/\w/g, '*')) // ***** ****! ***
console.log(str.replace(/\W/g, '*')) // hello*word**520
console.log(str.replace(/\b/g, '*')) // *hello* *word*! *520*
console.log(str.replace(/\B/g, '*')) // h*e*l*l*o w*o*r*d!* 5*2*0
console.log(str.replace(/\s/g, '*')) // hello*word!*520
console.log(str.replace(/\S/g, '*')) // ***** ***** ***

3.3 贪婪和惰性

贪婪匹配是最多情况下匹配内容

惰性匹配是最少情况下匹配内容

先看栗子

const reg = /a[bc]*c/g  // 贪婪
const str = 'abcbcbc'

console.log(str.match(reg)) // [ 'abcbcbc' ]

const reg1 = /a[bc]*?c/g // 惰性
console.log(str.match(reg1)) // [ 'abc' ]

解释:

  • 贪婪: 由于 abcbcbc 一直到字符串最后都符合正则的规则,所以一直匹配到不符合位置
  • 惰性:只匹配一个符合规则的就返回,不继续匹配。

4. 正则进阶操作

4.1 具名组匹配

JavaScript 中提供了组匹配的能力。可以同过自定义的组名来获取匹配内容.

组的固定格式为 ?<组名>。然后通过获取结果的groups.具体的名称 来获取

const reg = /(?<name>[a-zA-Z]{3,6})(?<age>\d{2})/
const str = 'xiaoming23'

str.match(reg).groups.name // xiaoming
str.match(reg).groups.age // 23

4.2 位置匹配

  • ?=搜索内容:查看右侧内容是否符合条件, 查看后缀是否为要搜索的内容。
const reg = /(?=abc)/
const str = 'efabcefabc'

str.replace(reg, '*') // ef*abcef*abc

解释:

匹配 后缀为abc 这个字符的位置

  • ?!搜索内容:查看右侧内容是否不符合条件,查看后缀是否为要搜索的内容
const reg = /(?!abc)/
const str = 'efabcefabc'

str.replace(reg, '*') // *e*fa*b*c*e*fa*b*c*

解释:

匹配 后缀不为abc 这个字符的位置

  • ?<=搜索内容:查看左侧内容是否符合条件,查看前缀是否是要搜索的内容
const reg = /(?<=abc)/
const str = 'efabcefabc'

str.replace(reg, '*') // efabc*efabc*

解释:

匹配 前缀为abc 这个字符的位置。

  • ?<!搜索内容:查看左侧内容是否不符合条件
const reg = /(?<!abc)/
const str = 'efabcefabc'

str.replace(reg, '*') // *e*f*a*b*ce*f*a*b*c

解释:

匹配 前缀不为abc 这个字符的位置。

第四小节总结:

  1. 组匹配 ?<组名>
  2. 匹配后缀 ?=?!
  3. 匹配前缀 ?<=?<!

5. 正则和字符串方法介绍

5.1 字符串方法

下述方法都支持正则匹配方式来操作。

  • replace(reg, 替换的内容 或者 一个操作函数) 替换

    此方法的第二个参数是比较神奇的,可以接收要替换的内容,也可以接受一个函数。

    • 为替换内容(1)
    const str = 'abc'
    str.replace(/a/, '*') // *bc
    • 为替换内容(2)

      可使用 $1,$2………… 等变量作为匹配到的组的内容。

    const str = '123abc'
    str.replace(/(a)(b)(c)/, '$1$3$2') // 123acb

    说明:

    $1 代表 (a) 所匹配到的内容

    $2 代表 (b) 所匹配到的内容

    $3 代表 (c) 所匹配到的内容

    替换的时候掉到顺序替换,则输出 123acb 这个值

    • 为函数
    const str = 'abc'
    str.replace(/(a)/, (source, $1, index) => {
      console.log(source, $1, index) // abc  a  0
      return *
    }) // *ab
    
    str.replace(/(b)(c)/, (source, $1, $2, index) => {
        console.log(source, $1, $2, index) // abc 
        return $1 + 1
    }) // a11

    说明:

    函数参数接收的参数,第一个source 为字符串本身,最后一个index为查找到的第一个索引值。中间的 $1, $2, $3……………… 为正则表达式里小括号的数量。返回值的作用是作为替换内容替换原字符串。

  • match 搜索

    根据正则的规则匹配字符串

    const reg = /[abc]/g
    const str = 'abc'
    
    str.match(reg) // [ 'a', 'b', 'c' ]
  • split 切割

    const str = 'abcabcabc'
    
    str.split(/ca/) //[ 'ab', 'b', 'bc' ]

    此方法还可接收第二个参数。length 设置返回的数组长度

    const str = 'abcabcabc'
    
    str.split(/ca/, 2) //[ 'ab', 'b' ]

5.2 正则方法

  • test 查看字符串是否符合正则规则

    const reg = /abc/
    const str = 'abc'
    const str1 = 'ab'
    
    reg.test(str) // true
    reg.test(str1) // false
  • exec 根据正则的规则匹配字符串。同 match

    const reg = /abc/
    const str = 'abc'
    const str1 = 'ab'
    
    reg.exec(str) // 'abc'
    reg.exec(str1) // null

6. 实战篇

理解了内容之后怎么也得练练手啊,此内容给大家准备了几个常见但是不太好理解的正则,请大家练手。

6.1 匹配 html 标签 (包含标签中的内容)

首先,我们来创建个字符串

const html = '<div></div>'

现在我们来抒写一下可描述标签的正则表达式。有以下几个特征:

  1. < 开头
  2. 标签名为英文字符
  3. </标签名> 结尾
第一版表达式
const reg = /<(\w+)><\/(\1)>/g

// 验证一下
html.match(reg) // '<div></div>'

\1引用上一个分组的正则规则上面的内容已经说过。

看我们的验证结果,完美。但是,有个问题。我们的标签一般都不在一行内书写,标签之间会有 \n 来标识换行。ok,让我们修改一下 html 字符串

const html = `<div>

</div>`

// 再次验证
html.match(reg) // null

😰完了,失败了。

不要着急,我们只匹配了标签,并没有匹配到标签中的内容。由于 . 并不能匹配到 \n 所以我们使用其他条件来匹配。

第二版表达式
const reg = /<(\w+)>([\s\S]*)<\/(\1)>/g 

// 验证
html.match(reg) // '<div>\n\n</div>'

(^o^)/,成功。

这里我们可以看到。使用 [\s\S] 可以匹配到 \n。因为 [\s\S] 代表的是空格和非空格。相同的用法还有 [\w\W]、[\b\B]、[\d\D]

第三版表达式

html 除了双标签还有单标签,下面我们来看下单标签的验证。单标签的开始跟双标签一样。但是结束不一样,单标签是以/>结束。不说了,先写一下

let reg = /<(\w+)/    // 相同的开头

// 结束规则书写
reg = /<(\w+)\/>/ 

注意,这里,单标签中的内容,我们不需要用 [\s\S] 这种方法验证,会有其他问题,因为我们需要匹配的是属性,匹配到换行符结束。所以下面这种写法就可以。

reg = /<(\w+)([^>]*)\/>/

[^>] 表示只要不是结束符号的,都符合条件。* 表示出现零次或多次

不说了,我们来验证下

const html = '![]()'
html.match(reg) // ![]()

😄,成功。

结合版(不是终极版)

结合版的正则表达式我们需要用到元字符中的|来作为分支判断

const reg = /<(\w+)>(([\s\S]*)<\/(\1)>)|(([^>]*)\/>)/mg
// (([\s\S]*)<\/(\1)>) 双标签
// (([^>]*)\/>)单标签

说明:m 的作用是来表示匹配多行。g 的意思是表示全局匹配

验证一下

const html = '<div title="1"></div><p></p>![](asfs)<br />'

html.match(reg)
// [ '<p></p>', '![](asfs)', '<br />' ]

成功!!

虽然这次的匹配成功了,但是这个表达式还是有很多问题,期待读者来完善哟。

6.2 实现数字千分位

同样,我们来分析一下需求。千分位是每隔三个数字,加上一个逗号。首先,我们先创建一个数字的字符串

const str = '12345678'

查看后缀是否是三个数字,我们使用 ?= 来做,它的作用就是查看后缀是否符合规则。先来创建正则

第一版
const reg = /(?=\d{3})/g

因为需要匹配整个数字,所以我们用到了 g 来表示全局匹配。好,正则创建完成,验证一下。

str.replace(reg, ',') // ,1,2,3,4,5,678

整个结果,好像有问题啊。

不着急,继续向下看,由于我们要数字三个一对的出现,所以这里我们需要添加出现一次或多次的校验。使用+

第二版
const reg = /(?=(\d{3})+)/g

先不急着验证,因为还没有完成。由于每三个字符是一个节点,所以这里还需要用 $ 来表示查找结束。

第二版改进
const reg = /(?=(\d{3})+$)/g

验证:

str.replace(reg, ',') // 12,345,678

貌似是成功了,多来验证几次。

…………

验证到这个字符串时出现问题。

const str = '123456789'
str.replace(reg, ',') // ,123,456,789

这里我们看到,数字正好是九位,所以开始的 123 也符合条件。所以需要吧开头禁止掉。使用 ?!这种方式

第三版
const reg = /(?!^)(?=(\d{3})+$)/g

再次验证:

str.replace(reg, ',') // 123,456,789

7. 总结

好了,本次的内容我们就先分享到这里了,期望你能从这篇文章中真正了解到正则表达式。本篇文章一共分享了一下几个内容

  • 元字符
  • 位置匹配
  • 字符串和正则表达式方法讲解
  • 实战操作

正则的使用方式千变万化。所谓能力越大,危害就越大,只有真正掌握了它,才能在实际应用中得心应手,否则容易造成不小的祸端。

那么,再见了,亲爱的读者朋友们,期待下次相遇~~

查看原文

赞 0 收藏 0 评论 1

yancy 发布了文章 · 2月4日

Object重识(二)

接上文:Object重识(一)

3. 对象继承

对象继承,我们不介绍名词,不说是什么样的继承,只从头看下每个继承方式的不足,并介绍如何补充

3.1 最简单的继承

function A (name) {
  this.name = name
}

function B () {}

B.prototype = new A('张三')
console.log(new B("李四").name) // 张三

此方法是将 B 的原型直接指向 A 的原型,从而达到继承的目的。Object.create方式创建对象等同于此方式。

优势:

可达到属性复用。

缺陷:

无法在实例化的时候传递参数,不可配置。上述示例中 AB 的名称都为 张三。即使我们在实例化 B 的时候传入一个名称。

3.2 改进版

改进版可在实例化的时候添加参数

function A (name) {
  this.name = name;
}

function B (name) {
  A.call(this, name)
}

console.log(new B('李四').name) // 李四

优势:

可实现定制化,支持传参配置

缺陷:

无法复用,所有内容都通过构造函数定义

3.3 结合版

function A (name) {
  this.name = name;
}
A.prototype.address = '中国'

function B (name) {
  A.call(this, name)
}
B.prototype = new A();

const b = new B('李四')
console.log(b.name) // 李四
console.log(b.address) // 中国

总结:

此继承方式也不是完整版,只是让大家了解继承的实现方式。像什么 寄生继承、寄生组合继承。咱们就多啰嗦了,容易越看越乱。有兴趣的读者可以之后再跟大家分享或者自行查阅。

3.4 ES6 继承

有了ES6之后感觉空气都甜了。之前复杂的继承写法都变得特别简单。看栗子↓↓↓↓↓

class A {
  constructor (name) {
    this.name = name;
  }
}
class B extends A {
  constructor (name) {
    super(name)
  }
}

const b = new B('李四')

console.log(b.name)

class方式创建对象注意点:

  • 写在 构造函数constructor 中的方法都属于自身的方法。
  • 写在构造函数constructor 之外的方法都属于原型方法

举栗说明:

class A {
  constructor () {
    this.log = function () {
      console.log('111111')
    }
  }
  log () {
    console.log('222222')
  }
}

const a = new A()
a.log() // 111111 -- 原型链的查找问题

console.log(a.log)
/*
    ƒ () {
    console.log('111111')
  }
*/

console.log(a.__proto__.log)
/*
  ƒ log () {
    console.log('222222')
  }
*/

4. 对象操作

4.1 对象转字符串

使用 JSON.stringify 方法将对象转换为字符串显示

const obj = {
  a: 1,
  b: 2,
  c: 3
}

const strObj = JSON.stringify(obj)

console.log(strObj) // {"a":1,"b":2,"c":3}

注意点:

JSON.stringify(obj, replacer, space) 接收三个参数

  • obj 被转换的对象
  • replacer 可以是 函数 或 数组。为数组,只转换数组中的值
const obj = {
  a: 1,
  b: 2,
  c: 3
}

// 第二个参数为数组
console.log(JSON.stringify(obj, ['a', 'b'])) // {"a":1,"b":2}

// 第二个参数为函数
function replacer(key, value) {
  if (value === 1){
    return undefined
  }
  return value
}
console.log(JSON.stringify(obj, replacer)) // {"b":2,"c":3}
  • space 可以为数字或字符串,添加输出结果中的间距。最大间距数量为 10 。最小为 0
const obj = {
  a: 1,
  b: 2,
  c: 3
}

// 为数字
console.log(JSON.stringify(obj, null, 2))
/*
{
  "a": 1,
  "b": 2,
  "c": 3
}
*/

// 为字符串
console.log(JSON.stringify(obj, null, '-')) 
/*
{
-"a": 1,
-"b": 2,
-"c": 3
}
*/

4.2 对象转换为数组

1. Object.keys 返回对象的 key 值组成的数组
2. Object.values 返回对象的value值组成的数组
3. Object.entries 返回对象的 key 值和value值组成的数组
const obj = {
  a: 1,
  b: 2,
  c: 3
}

console.log(Object.keys(obj)) // [ 'a', 'b', 'c' ]
console.log(Object.values(obj)) // [ 1, 2, 3 ] 
console.log(Object.entries(obj)) // [ [ 'a', 1 ], [ 'b', 2 ], [ 'c', 3 ] ]

上述方法会用就可以,没太多需要注意的点。

4.3 对象扩展,密封,冻结

1. 对象拓展

什么是对象拓展?

1.1小节中创建的对象都可以添加新的属性和方法,这就叫对象拓展。可以使用 Object.isExtensible 方法查看。

function B () {}

const a = {}
const b = new B()

console.log(Object.isExtensible(a)) // true
console.log(Object.isExtensible(b)) // true

禁止对象拓展, 可使用 Object.preventExtensions 方法

function B () {}

const a = {}
const b = new B()

// 禁止对象拓展
Object.preventExtensions(a)
Object.preventExtensions(b)

console.log(Object.isExtensible(a)) // false
console.log(Object.isExtensible(b)) // false
2. 对象密封 Object.seal

对象密封可以理解为字面的意思。就是把一个对象密封起来。

密封的对象不可拓展(不能添加新属性),不可删除属性,不能修改已有属性的属性描述符(writeable、value等)

将一个对象密封,可以使用Object.seal方法

同样,查询一个对象是否是密封对象,可以使用Object.isSealed方法

const obj = {
  a: 1,
  b: 2,
  c: 3
}

// 密封一个对象
Object.seal(obj)

// 尝试删除
delete obj.a
console.log(obj) // { a: 1, b: 2, c: 3 }

// 尝试添加
obj.d = 4
console.log(obj) // { a: 1, b: 2, c: 3 }

// 查询对象是否是密封状态
console.log(Object.isSealed(obj)) // true

// 尝试修改原有属性值
obj.a = 4
console.log(obj) // { a: 4, b: 2, c: 3 }

注意:

虽然将对象密封了,但是依旧可以修改原有的属性值。如果想让一个对象不可更改,可以用到下面这个方法。

3. 对象冻结 Object.freeze

对象冻结顾名思义就是将对象的所有属性都冻结住,不可修改,不可增删。可以使用Object.freeze方法冻结一个对象。查看一个对象是否是冻结对象可以使用Object.isFrozen() 方法。

const obj = {
  a: 1,
  b: 2,
  c: 3
}
// 冻结一个对象
Object.freeze(obj)

// 尝试新增
obj.d = 4
console.log(obj) //{ a: 1, b: 2, c: 3 }

// 尝试删除
delete obj.a
console.log(obj) //{ a: 1, b: 2, c: 3 }

// 尝试修改
obj.a = 4
console.log(obj) //{ a: 1, b: 2, c: 3 }

// 查看对象是否冻结
Object.isFrozen(obj) // true

看吧,冻结之后的对象你是不能对它做操作的。

但是………………

这种情况下可以修改

const obj = {
  a: 1,
  b: 2,
  c: {
    d: 4
  }
}
Object.freeze(obj)

obj.c.d = 11
console.log(obj.c) //{ d: 11 }

也就是说,冻结对象只能冻结一层,不能深层次冻结,如果需要深层次冻结,则需要递归的冻结每个值为对象的属性。

4.4 对象解构

此操作等同于数组解构,不同的是,对象会根据属性值进行解构操作。话不多说,小栗子来看下

const obj = {
  a: 1,
  b: 2,
  c: 3
}

// 普通解构
let {a, b} = obj
console.log(a, b) // 1, 2

// 带默认值的解构,解构不到时,赋值为默认值
let {a = 0, b = 0, d = 0} = obj
console.log(a,b, d) // 1,2,0

// 解构并重命名
let {a: newA, b: newB} = obj
console.log(newA, newB) // 1, 2

// 默认值 and 重命名
let {d: newA = 0, b: newB = 0} = obj
console.log(newA, newB) // 0, 2

注意点:

  • 解构可以添加默认值
  • 解构可以重命名
  • 可以使用重命名 and 默认值并存的方式解构对象

4.5 对象拷贝和对象比较 Object.assign、Object.is

1. 对象拷贝

Object.assign 可以实现对象的复制,合并,深拷贝等操作,需要注意的点是:继承的属性和不可枚举(enumerable为false)的属性不能拷贝

  • 对象拷贝
// 深拷贝

const obj1 = {
  d: 4,
  e: 5
}

const a = Object.assign({}, obj1)

console.log(a)

obj1.d = 2
console.log(a) // 修改之后不会变动a的值
  • 对象合并
const obj = {
  a: 1,
  b: 2,
  c: 3
}

const obj1 = {
  a: 4,
  e: 5
}

const a = Object.assign(obj, obj1)

console.log(a) // { a: 4, b: 2, c: 3, e: 5 }

合并后,如果第二个参数中有和第一个参数相同的属性,则第二个参数的属性会覆盖第一个参数的属性。

2. 对象比较

之前的工作中,我们会知道这样一句话,NaN不等于任何东西,包括它自身 。那么我们用Object.is来试一下。

此方法用来比较两个对象是否相等。

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

除去上面这种案例,其他的表现与 === 相同

4.6 ?.?? (可选链和空值合并)

1. 可选链 ?.

日常工作中,像下面这种代码非常常见。

const obj = {
  result: {
    a: 1
  }
}

let result = obj.result && obj.result.a;

当然,有了可选链之后,我们就不需要这么麻烦了 ,上述代码可改写成以下方式:

const obj = {
  result: {
    a: 1
  }
}

let result = obj.result?.a;

如果obj.resultundefined 或者 null 时,会中断操作而返回undefined 或者 null

此方法也可用于函数

const obj = {
  result: function (){

  }
}

obj.result?.();

用于数组:

const arr = []
arr?.[0]

用于表达式:

const obj = {
  parentResult
}

obj?.['parent' + 'Result']

可连用:

const arr = [[[1]]]

arr?.[0]?.[0]?.[0]

可选链具有短路功能,如果左边的操作数为 undefinednull ,则会中断表达式计算。 (类似于 &&)

但是,此方式不能用于赋值

obj?.['parent'] = 1
2. 空值合并

如果我们相给一个值赋默认值,通常是这么做

let obj = {}
let result = obj.a || 1

现在,我们可以这么做

let obj = 0
let result = obj.a ?? 1 

空值合并同样具有短路功能,当左边不为null时,中断操作。(类似于 || )

注意:

控制合并不能与 &&|| 连用哟。会出现问题。

?.??合用

let obj = {
  a: 1,
  b: 2
};
let result = obj?.a ?? 0;
console.log(result); // 1

好了,every body. 今天要分享的内容就到这里了。咱们下次再见~~

查看原文

赞 0 收藏 0 评论 0

yancy 关注了用户 · 1月25日

修仙大橙子 @xiuxian_orange

前端工程师

关注 3

yancy 发布了文章 · 1月25日

Object 重识(一)

内容有点儿多,将分为两篇内容与大家分享。有的地方确实难以理解,可以多动手实现一下,看看效果。

一直听说有这么一句话,在JavaScript中,一切皆为对象。

经本人慎重研究之后发现,此言的确不虚。(这不废话么,虚不虚的还用你说。)

今天斗胆跟大家一起讨论一下JavaScript重的Object。了解Object背后的 -- 生活

阅读间隙,还得多注意休息哟~

1. 对象的创建

开始第一个小问题,看一下如何创建一个对象。创建对象有四(shi)种方式。以下几种方式都可创建一个对象,只不过创建之后的表现稍有不同。具体表现为原型和this的指向问题。有兴趣的读者可点击查看相关文章。在这里我们就不详细介绍了。(偷个懒)

彻底弄懂JavaScript原型问题

深入理解JavaScript的 this 指针

注意点:

四种方式创建对象,除了原型this指针表现不同,构造函数指向也有不同。(此时说明的是不手动指定构造函数的情况下)

  • 字面量方式创建的对象。构造函数为 function Object() {}
  • 构造函数创建。构造函数为函数本身
  • Object.create创建的对象,构造函数为入参origin的构造函数
  • class 方式创建,会将本身也作为构造函数。

1.1 字面量方式创建

const obj = {} // 构造函数为 [Function: Object]

1.2 构造函数创建

function Obj() {}

const obj = new Obj() // 构造函数为 [Function: origin]

1.3 Object.create 创建

const origin = {a: 1, b: 2}

const target = Object.create(origin) // 构造函数为 origin 的构造函数

1.4 class方式创建

class Origin {}

const target = new Origin() // 构造函数 [Function: Origin]

2. 属性和方法

注:

  • 官方版

    • 属性是这个对象的属性,方法是这个对象所拥有的功能
  • 通俗版

    • 属性是一个值,不可执行。
    • 方法为一个函数,可执行。

2.1 属性和方法获取

1.获取确定的key属性,使用 . 运算符 或 中括号
const obj = {
  a: 1,
  b: 2,
  c: 3,
  d: function () {}
}

obj.a === obj['a']// 1
obj.b === obj['b'] // 2
obj.c === obj['c'] // 3
obj.d === obj['d'] // function (){}
2.获取需要计算的kay值,只能使用中括号方式
const obj = {
  a1: 1,
  a2: 2,
  a3: 3,
  a4: function (){}
}

for(let i = 1;i < 5;i ++) {
  obj['a' + i]
}
// 1,2,3,function (){}

注意:

如果对象中不含有某个key,获取到的为 undefined

例如:

const obj = {}

obj.c() // TypeError: obj.c is not a function

由于obj中不含有c这个方法,所以获取到的是undefined。此值不是函数,执行报错

可修改为以下写法

const obj = {}

typeof obj.c === 'function' && obj.c()

这里有的读者可能会说这样也可以 obj.c && obj.c()。这种写法在下面这种情况下依然会报错

const obj = {
  c: 1
}

obj.c()

所以,还是要清楚的判断我们要获取的key是一个函数,才能执行。

2.2 如何判断对象中含有某个值

在此段内容开始之前,先说一个小问题,如何判断对象中是否含有某个key值?

1. 使用 2.1中的属性获取
  • 只需判断是否为 undefined 即可。
  • 此方法有一个缺陷,如果此值确实为 undefined,则会出现判断失误的情况
const obj = {
  c: undefined
}

console.log(obj.c === 'undefined') // 判断为true,但是obj中确实含有c这个key
2. in
  • 包含某个key
// 包含某个key
function A () {
  this.b = 1
}
const a = new A()
console.log('b' in a) // true
  • 不包含某个 key
// 不包含某个key
function A () {
}
const a = new A()
console.log('b' in a) // false
  • 查询的key,值为 undefined 的情况
// 不包含某个key
function A () {
  this.b = undefined
}
const a = new A()
console.log('b' in a) // true
  • 查询的key存在于原型链上
function A () {
}

Object.prototype.b = 1
const a = new A()

console.log('b' in a) // true

注意点:

缺点:in方法无法判断当前key是存在于对象本身还是在原型链上。此情况,我们需要用到下面这种方法。

3. hasOwnProperty 查询属性是否存在于自身
  • 存在于原型链上
function A () {}

Object.prototype.b = 1
const a = new A()

console.log(a.hasOwnProperty('b')) // false
  • 存在于自身
function A () {
  this.b = 1
}

const a = new A()

console.log(a.hasOwnProperty('b')) // true
  • 属性值为undefined
function A () {
  this.b = undefined
}

const a = new A()

console.log(a.hasOwnProperty('b')) // true

总结:

  • 属性获取不能判断属性值为undefined的情况
  • in 方法不能判断属性是在对象本身还是在原型链上
  • hasOwnProperty可以判断属性是否存在于自身,同时也可判断属性值为undefined的情况

2.3 删除对象的属性

删除对象的属性可以使用delete方法,只不过此方法的性能并不算太优越

const obj = {
  b: 1,
  c: 2
}

delete obj.b
console.log(obj) // {c: 2}

休息会儿,这篇文章可长哈,循序渐进的来。

2.4 属性分类

JavaScript中将属性分为两种:数据属性和访问器属性

数据属性包括:ConfigurableEnumerableWritableValue 四个属性描述符

访问器属性包括:ConfigurableEnumerableSetGet 四个属性描述符

通过对比我们可以发现,属性描述有六类:

Configurable、Enumerable、Writeable、Value、Set、Get

注意:

修改描述符,只可以通过Object.defineProperty(obj, key, config)修改一个 || Object.defineProperties(obj,config)修改多个 方法来修改。

例如:

const obj = {
  a: 1,
  b: 2,
  c: 3
}
// 修改一个
Object.defineProperty(obj, 'a', {
  enumerable: true,
  writable: true,
  configurable: true
})

// 修改多个

Object.defineProperties(person, {
  a: {
    enumerable: true,
    writable: true,
    configurable: true
  },
  b: {
    enumerable: true,
    writable: true,
    configurable: true
  }
}
1. 描述符介绍

Configurable:是否可通过delete删除属性、是否可重新定义属性、是否可修改描述符、是否可进行属性互换。

  • true 上述操作都可进行
const obj = {
  a: 1,
  b: 2,
  c: 3
}
Object.defineProperty(obj, 'a', {
  configurable: true
})

delete obj.a
Object.defineProperty(obj, 'a', {
  value: 11
})
Object.defineProperty(obj, 'a', {
  enumerable: true
})
Object.defineProperty(obj, 'a', {
  set() {}
})
  • false 严格模式下报错,普通模式下过滤操作
const obj = {
  a: 1,
  b: 2,
  c: 3
}
Object.defineProperty(obj, 'a', {
  configurable: false
})

delete obj.a
Object.defineProperty(obj, 'a', {
  value: 11
})
Object.defineProperty(obj, 'a', {
  enumerable: true
})
Object.defineProperty(obj, 'a', {
  set() {}
})

Enumerable:是否可枚举

const obj = {
  a: 1,
  b: 2,
  c: 3
}

// 为 true 的情况,可遍历
Object.defineProperty(obj, 'a', {
  enumerable: true
})

for(let item in obj) {
  console.log(item) // a, b, c  可遍历到 a
}

// 为false的情况,不可遍历
Object.defineProperty(obj, 'a', {
  enumerable: false
})

for(let item in obj) {
  console.log(item) // b, c  遍历不到 a
}

Writable:是否可修改属性的值,加上它,将变为数据属性

const obj = {
  a: 1,
  b: 2,
  c: 3
}

// 为 true 可修改属性值
Object.defineProperty(obj, 'a', {
  writable: true
})
obj.a = 2
console.log(obj.a) // 2

// 为false 不可修改属性值 严格模式下会报错
Object.defineProperty(obj, 'a', {
  writable: false
})
obj.a = 2
console.log(obj.a) // 1

Value:属性的数据值,加上它,将变为数据属性

const obj = {
  a: 1,
  b: 2,
  c: 3
}
Object.defineProperty(obj, 'a', {
  value: 11
})
console.log(obj.a) // 11

Set:写入属性时调用,加上它,将变为访问器属性

const obj = {
  a: 1,
  b: 2,
  c: 3
}
Object.defineProperty(obj, 'a', {
  set() {
    console.log('a的值改变了')
  }
})

obj.a = 2 // a的值改变了

Get:读取属性时调用,加上它,将变为访问器属性

const obj = {
  a: 1,
  b: 2,
  c: 3
}
Object.defineProperty(obj, 'a', {
  get() {
    console.log('获取a的值')
  }
})

obj.a // 获取a的值
2. 属性互换:

数据属性 --> 访问器属性

Writable、Value 中的任意一个替换为 Set、Get 中的任意一个。

访问器属性 --> 数据属性

Set、Get 中的任意一个替换为 Writable、Value 中的任意一个。

示例可接着往下看

3. 获取某个属性的描述(查看此属性是数据属性还是访问器属性)

想要查看某个属性的描述,可以使用Object.getOwnPropertyDescriptor(obj, key) 来查看

const obj = {
  a: 1
}
console.log(Object.getOwnPropertyDescriptor(obj, 'a'))
// { value: 1, writable: true, enumerable: true, configurable: true }

Object.defineProperty(obj, 'a', {
  get() {
    console.log('获取a的值')
  }
})
console.log(Object.getOwnPropertyDescriptor(obj, 'a'))
/*
{
  get: [Function: get],
  set: undefined,
  enumerable: true,
  configurable: true
}
*/

此例我们可以验证 属性互换 的正确性,当我们将get设置为 a 的描述符时,a 会变为访问器属性

接下文:Object 重识(二)

查看原文

赞 0 收藏 0 评论 0

yancy 发布了文章 · 1月20日

Array,重新认识一下

本文的内容api方法较多,千万,千万,千万,千万,千万,别死记硬背。没有任何作用,反而加重你学习的负担,碰到有合适的场景可以查api来使用。

又是一篇基础文章,为什么要这么强调基础呢?在之前的面试中,发现好多面试者将精力放到了框架、类库上。对于基础的东西浅尝辄止。殊不知这样学习如同南辕北辙。学不好基础怎么能将更高级的东西了解透彻呢。

文章意为让大家提高对基础知识的认知,更好的学习,更快的学习。很多时候你会发现一个问题,编程的知识都是互通的,一通则百通。所以呢?还是底层,还是基础。地基之深厚,方能使知识的大厦屹立不倒。

一不小心废话又多了,请相信我,基础写完之后,一定会有进阶知识点。怎么样,期待不。😸

闲言少叙,今天咱们来认识一下数组这个东西。

every body,开始我们的表演……

1.常规操作

数组是在内存中连续存储数据的容器,形如你看到的酒店房间、火车、教室……都是连续的。

数组的特点是增加、删除慢,更新快。为什么这么说呢?

因为在数组中添加和删除元素,会将后面的元素依次后移或者前移。

小栗子上场

const arr = [1,2,3,4]

// 添加元素
arr.push(0) // 会将4,3,2,1分别向后移动一位,给0这个新元素腾出位置

// 删除元素
arr.shift() // 删除后,由于第一位空出,所以会将后面的元素依次前移。

// 更新元素
arr[0] = 10 // 只修改第一个元素,不影响其他

总结:

  • 每次增加和删除元素都会同时操作后面的元素
  • 更新不会影响其他元素,只会影响当前元素

下面这个表格详细的描述了每一个常用方法的功能和使用。敬请查阅。

1.1 小文档:

作用方法返回值是否影响原数组
增加(后)push添加后的数组长度
删除(后)pop删除的元素
增加(前)unshift添加后的数组长度
删除(前)shift删除的元素
修改索引修改
查询索引查询查询到的元素
拼接concat返回拼接后的数组
裁剪slice裁剪到的数组
排序sort排序后的数组
转换转为字符串 join转换后的字符串
万能方法splice操作的数据

1.2 小示例:

pop、unshift、shift、索引修改、索引查询 这几种方法都是常用方法,不必太过多说。

其他方法使用时的注意点:

1. push
let arr = [1,2,3]

// 增加单个元素
arr.push(4) // [1,2,3,4]

// 添加多个元素
arr.push(5,6) // [1,2,3,4,5,6]

// 增加数组
arr.push([7,8]) // [1,2,3,4,5,6,[7,8]]

说明:push添加数组时,会将数组作为一个元素添加到原数组中,最终得到一个多维数组

2. concat
let arr = [1,2,3]

// 拼接一个元素
arr.concat(4) // [1,2,3,4]

// 拼接多个元素
arr.concat(5,6) // [1,2,3,4,5,6]

// 拼接数组
arr.concat([7,8]) // [1,2,3,4,5,6,7,8]

说明:concat拼接数组时,会将数组里的每一项单独添加到原数组中。

3. slice
let arr = [1,2,3]

// 只传一个参数,从当前位置一直裁剪到数组末尾
arr.slice(0) // [1,2,3]

// 传两个参数,从第一个的位置裁剪到最后一个的位置,包含起始位置,不包含结束位置
arr.slice(0,1) // [1]

// 若第二个参数大于第一个参数。截取为空
arr.slice(1,0) // []

说明:slice 的两个参数都可以接收负数负数的意义为从后向前查找,所起的作用和代码中相同。

4. sort
let arr = [1,2,3]

// 正序排列
arr.sort((a,b) => a - b)

// 倒序排列
arr.sort((a,b) => b - a)

说明:sort 可接收一个排序函数作为参数,正序为参数1 - 参数2,倒序为参数2 - 参数1

5. join
let arr = [1,2,3]
// 元素之间可使用指定的连接符连接
arr.join('-') // 1-2-3
arr.join('=') // 1=2=3
……
6. splice
let arr = [1,2,3]

// 任意位置新增
arr.splice(0,0,'11') // ['11',1,2,3]

// 任意位置删除
arr.splice(0,1) // [1,2,3]

// 任意位置替换任意个数的元素
arr.splice(1,2,4,5,6) // [1,4,5,6]

说明:splice(索引位置,修改个数,新数据1,新数据2,新数据3,……)

  • 修改个数为0,表示新增
  • 新数据个数为0,表示删除
  • 其他情况为,从索引位置开始,向后查找 修改个数 个元素,修改为后边所有的新数据。

2.骚操作 (进阶操作)

说完了基础,怎么能不进阶呢。就像你吃腻了粗茶淡饭,怎么也得来点儿大鱼大肉吧。

接下来,就是满汉全席的时刻~~~

1.使用 from 创建数组
const arr = [1,2,3];

// 通过arr创建新数组。
const arr1 = Array.from(arr);

// 创建新数组,并且单独处理每一项
const arr2 = Array.from(arr, item => item * 2)

// 创建定长数组并使用0填充
const arr3 = Array.from({length: 10}, () => 0)

注意:

from 可接收第二个参数,可以将每一项经过运算,并返回处理后的数组。

2.扁平化数组 flat
[[1,2]].flat()
[[1,2]].flat(1)
[[[[[[1,2]]]]]].flat(Infinity)

注意:

  • flat 可接收一个参数,为扁平化的层级
  • 如果传入 Infinity 则表明,不管数组有多少层。它都能给扒光。
3.去重处理new Set()
let arr = [1,1,2,2,3,3,4,4,5,5,6,6]
arr = [...new Set(arr)] // [1,2,3,4,5,6]

注意:

  • set 是一种不包含重复元素的集合。可以通过数组创建
  • 利用set的去重特性,过滤一遍数组,得到的就是不包含重复元素的数组
4.数组排序sort -- 数组乱序 - 0.5
[3,2,1].sort((a,b) => a - b) // 正序
[3,2,1].sort((a,b) => b - a) // 倒序

// 创建一个乱序的数组
[1,2,3,4,5,6,7,8,9,10].sort(() => Math.random() - 0.5)

注意:

  • sort接收一个函数作为排序函数。函数返回 正数 为正序,负数 为倒序,0 为相等
  • 使用随机数,可以不确定的返回正数、负数和0。也就会得到一个乱序的数组
5.最大和最小值 Max和Min
Math.max(...[1,2,3,4,5]) // 5
Math.min(...[1,2,3,4,5]) // 1

注意:

  • maxmin函数只接收参数,不能接受数组。所以要将数组中的元素展开传入。
6.数组处理高阶函数reduce、map、filter、some、every、forEach、fill

(这可是个大活儿。)

// reduce 聚合
[1,2,3,4].reduce((pre, next, index, array) => pre + next)
[1,2,3,4].reduce((pre, next, index, array) => pre + next, 0) // 第二个参数为默认值

// map 遍历处理
[1,2,3,4].map((item, index, array) => item * 2) // [2,4,6,8]

// filter 过滤
[1,2,3,4].filter((item, index, array) => item > 2) // [3,4]

// some 部分符合条件
[1,2,3,4].some((item, index, array) => item === 1) // true
[1,2,3,4].some((item, index, array) => item === 5) // false

// every 全部符合条件
[1,2,3,4].every((item, index, array) => item === 1) // false
[1,1,1].every((item, index, array) => item === 1) // true

// forEach 遍历
[1,1,1].forEach((item, index, array) => console.log(item))

// fill 填充
new Array(10).fill(value, start, end)
new Array(10).fill(1, 0, 2) // [1,1,,,,,,,,]

注意:

入参函数的参数说明:

  1. map、filter、some、every、forEach 这几个函数的参数是一样的,都是(item, index, array)

    • item 是当前操作的元素
    • index 是当前的索引值
    • array是操作的数组本身。
  2. reduce((pre, next, index, array) => {}, defaultValue)

    • pre是当前操作的结果
    • next为下一个要操作的元素。
    • index 是当前的索引值
    • array是操作的数组本身
    • defaultValue为默认值
  3. fill(value, start, end)

    • value为填充的值
    • start为开始位置(包含)
    • end为结束位置(不包含)
7.includes、indexOf、lastIndexOf、find、findIndex
// 是否包含 includes
[1,2,3,4].includes(4) // true
[1,2,3,4].includes(5) // false

// 查找首次出现的位置 indexOf
[1,2,3,4].indexOf(3) // 2

// 查找最后一次出现的位置 lastIndexOf
[1,2,3,4].lastIndexOf(3) // 2

// 查找元素 find
[1,2,3,4].find(item => item === 3) // 3
               
// 查找元素的索引
[1,2,3,4].findIndex(item => item === 3) // 2

注意:

这几个方法没什么可注意的,会使用就ok了。😂😂😂

8.Array.isArray
// 判断是不是数组
const arr = 1
Array.isArray(arr) // false
Array.isArray([]) // true
9.数组解构
const arr = [1,2,3,4,5,6]
const [a,,b,...c] = arr // 将 0 赋值给 a。将3赋值给b。将 4,5,6 作为一个数组赋值给c

// 小技巧:
// 通过解构交换两个值
let a = 1
let b = 2
[a, b] = [b, a]
console.log(a, b) // 2, 1
10.展开操作符
const arr = [1,2,3]
console.log(arr) // [1,2,3]
console.log(...arr) // 1,2,3

注意:

是将数组中的每个元素做展开处理。

吼吼吼,终于看完了,见底了,记住,千万别死记硬背哟。(赶紧休息休息,看到这里的你,肯定脑子累的不行不行的。)

查看原文

赞 0 收藏 0 评论 0

yancy 发布了文章 · 1月15日

玩转事件循环

1. 前言

这篇文章是想跟大家一起讨论一下javascript中高大上的Event Loop事件循环机制

代码得仔细分析,才能绕的过来,否则容易绕晕,谨慎再谨慎~~~

事件循环主要讲的是异步执行问题,没办法,同步就是顺序执行,没什么好说的。

<font color=gray>至于什么线程、同步异步、调用栈、浏览器线程之类的概念,本文一概没有,有意者可自行查阅。</font>

2. 简介

2.1 任务队列

  • 宏任务队列:script整体代码、setTimeout/setInterval……
  • 微任务队列:Promise

任务队列在事件循环机制中发挥着核心作用。是我们了解事件循环机制的核心要素。

2.2 执行顺序

1. 无 async/await 的执行顺序

注意点前置:

  • Promise中的函数属于同步函数

    • new Promise(function a(resolve, reject) {
        console.log('此为同步函数')
      })
  • setTimeout、setInterval、 Promise.then等函数,不为完成状态的情况下,不会推送到任务队列中

图解:

image

流程图执行说明:

  1. 首先执行同步代码
  2. 执行结束后查看微任务队列是否有任务,有则执行
  3. 微任务队列清空,查看宏任务队列是否有任务,有则执行
  4. 循环 2 和 3 。直至两个队列都清空
  5. 执行结束

栗子:

console.log('1');

setTimeout(function() {
    console.log('2');
}, 0)

new Promise(function(resolve) {
    console.log('3');
    resolve();
}).then(function() {
    console.log('4');
});
console.log('5')

分析:

  1. 首先执行同步代码,上述代码中 console.log(1)、console.log(3)console.log(5) 都属于同步代码。优先输出。同时执行延时函数、then函数,then函数立即返回结果,推送到微任务队列
  2. 检查微任务队列,有 console.log(4) 任务,执行并输出。微任务队列结束。
  3. 延时函数倒计时完成推入宏任务队列。
  4. 检查宏任务队列,有 console.log(2) 任务,执行并输出。宏任务队列结束。
  5. 代码执行结束。
  6. 输出顺序:1,3,5,4,2

2.async和await后的时间循环机制

<font color=red>提醒: 部分文章会介绍 await 让出执行权的问题,这里先不讨论,容易绕晕。有兴趣的可以查看以下。</font>

await代码的执行:

  • async标记的函数属于同步函数

    • async function log () {
        console.log('同步函数')
      }
  • await函数中返回一个promise,则执行后为 pending 状态,将下方的代码作为微任务处理,并且,只有await函数执行之后才会执行下方代码(请看栗子4)

    • async function async2() {
        return new Promise(function(resolve) {
          console.log('3');  // 3
          resolve();
        }).then(function() {
          console.log('4'); // 6
        });
      }
      console.log(async2()) // Promise { <pending> }
  • await函数中执行一个promise但不return 或者 resolve 被 setTimeout/setInterval 包裹,则执行后为 结束 状态,将下方的代码作为同步代码执行

    • // await函数中执行一个promise但不return
      async function async2() {
        // 区别在于没有return
        new Promise(function(resolve) {
          console.log('3');  // 3
          resolve();
        }).then(function() {
          console.log('4'); // 6
        });
      }
      console.log(async2()) // Promise { undefined }
      
      
      // resolve 被 setTimeout/setInterval 包裹
      async function async2() {
        new Promise(function(resolve) {
          console.log('3');
          setTimeout(() => {
            resolve();
          }, 0)
        }).then(function() {
          console.log('4');
        });
      }
      console.log(async2()) // Promise { undefined }

栗子1:

async function async1() {
  console.log('1');
  await async2();
  console.log('2');
}
async function async2() {
  return new Promise(function(resolve) {
    console.log('3');  
    resolve();
  }).then(function() {
    console.log('4'); 
  });
}
console.log('5'); 

setTimeout(function() {
  console.log('6'); 
}, 0)
async1();

new Promise(function(resolve) {
  console.log('7'); 
  resolve();
}).then(function() {
  console.log('8'); 
});

console.log('9');

解析:

  1. 先执行同步,console.log(5)、console.log(1)、console.log(3)、console.log(7)、console.log(9),将console.log(4)、console.log(8)放入微任务队列。将console.log(6)放入宏任务队列。
  2. 执行console.log(4) 由于async2返回的是 promise 所以将后面的代码放入微任务队列中。此时微任务队列有两个任务 console.log(8)、 console.log(2) 。清空微任务队列,输出 8 和 2
  3. 执行宏任务队列。输出 6
  4. 输出顺序 5,1,3,7,9,4,8,2,6

栗子2(栗子1的变种):

// 将async2改为下面这种写法,其他代码不变
async function async2() {
  new Promise(function(resolve) {
    console.log('3');  
    resolve();
  }).then(function() {
    console.log('4'); 
  });
}

解析:

  1. 栗子1第一步保持不变
  2. 改变第二步,执行console.log(4) 由于async2返回的不是 promise 所以直接执行后面的代码。输出 2此时微任务队列只有 console.log(8) 清空微任务队列,输出 8
  3. 栗子1第三步保持不变
  4. 输出顺序:5,1,3,7,9,4,2,8,6

栗子3(依旧是栗子1的变种,最后一个小栗子):

async function async1() {
  console.log('1');
  await async2();
  console.log('2');
}
// 将async2中的resolve函数使用setTimeout包裹 且 return一个Promise
async function async2() {
  return new Promise(function(resolve) {
    console.log('3');
    setTimeout(() => {
      resolve();
    }, 0)
  }).then(function() {
    console.log('4');
  });
}
console.log('5');

setTimeout(function() {
  console.log('6');
}, 0)
async1();

new Promise(function(resolve) {
  console.log('7');
  resolve();
}).then(function() {
  console.log('8');
});

解析:

  1. 先执行同步,console.log(5)、console.log(1)、console.log(3)、console.log(7)、console.log(9),将console.log(8)放入微任务队列。将console.log(6)和 console.log(4)放入宏任务队列。
  2. 执行微任务console.log(8),输出 8
  3. 执行宏任务console.log(6),输出6
  4. 由于async2返回一个Promise,所以只能等到async2执行之后才会将console.log(2)推入到微任务队列。
  5. 执行宏任务console.log(4),输出4并将 console.log(2)推入到微任务中。
  6. 执行微任务console.log(2),输出2
  7. 输出顺序:5,1,3,7,9,8,6,4,2

栗子4(真的是最后一个小栗子了):

// 将栗子3的async2函数改为如下写法
async function async2() {
  new Promise(function(resolve) {
    console.log('3');
    setTimeout(() => {
      resolve();
    }, 0)
  }).then(function() {
    console.log('4');
  });
}

解析:

  1. 栗子3第一步不变
  2. 由于async2不返回Promise,所以会将console.log(2)作为同步代码执行,第二步执行同步任务console.log(2) 输出2
  3. 执行微任务console.log(8),输出 8
  4. 执行宏任务console.log(6),输出6
  5. 执行宏任务console.log(4),输出4
  6. 输出顺序:5,1,3,7,9,2,8,6,4

无论你是否认真的查看了上面的文章,首先恭喜你可以看到这里,事件队列一直是比较难以理解的javascript知识点。还是衷心的希望这篇文章能带给你不一样的理解。也祝福正在看文章的你技术越来越好。

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 3 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-08-09
个人主页被 694 人浏览