JavaScript -senior
Senior JavaScript
Published: 2020-01-30

The article contains the senior knowledge of JavaScript…

类与对象

构造器

给类添加方法

类的继承

super关键字

super关键字调用规则

在构造器中写按钮的点击事件

这样写至少不会报错,但是还是会有问题,你会发现在点击btn按钮的时候他显示undefined

注意:方法这里不要加小括号:

不然就直接调用了,我们希望按了按钮之后再调用

this指向问题(跟原先的想法不太一样)

这个this.btn.onclick = this.sing之后sing()函数里面的this指向的是btn而非Star实例对象,所以才出现了上面点击btn之后他显示的不是uname而是undefined

那么怎么让这个按钮输出我们想要的内容呢?

这里解释一下(个人理解):由于btn自己的this和类的this两个重名了,那么在调用btn事件的时候会优先使用btn的this,所以才会导致this.uname为undefined,那事实上我们只需要给他换一个名称让他不重名即可,这里把this赋值给that,这个时候再点击btn他就会去调用that.uname了,就不会出现重名了,这个时候就能正常输出

面向对象版tab栏切换案例

tab栏切换功能

将刚才的点击事件封装到toggleTab方法中

toggleTab这个函数中的this指向的是调用者,因此如果我们想要拿到sections就不能用this了,我们可以用之前的方法,用一个全局变量代替this

封装清除其他tab属性的函数clearClass()

tab栏添加功能

insertAdjacentHTML()

我们使用createElement和appendChild添加元素的方式有时候太麻烦,可以直接用insertAdjacentHTML()

注意appendChild不支持追加字符串,他要加必须是createElement出来的元素,而insertAdjacentHTML支持追加字符串元素

position是相对于元素的位置,并且必须是以下字符串之一

beforebegin

afterbegin

beforeend

afterend

同理,是在当前元素外部的后面插入

添加元素

由于添加元素之后li和section是需要重新获取的,因此我们把li和section的获取函数封装出来

将获取li和section函数添加到初始化函数中,在每一次添加tab栏元素的时候都调用一次初始化函数即可

这样就不会出现添加元素之后新元素tab栏切换不起效了

tab栏删除功能

注意,x号是li的孩子,给x号添加按钮事件之后会发生冒泡,由于li也有按钮事件(tab栏切换),但是我们又不希望li的按钮事件发生,所以要组织冒泡

x号的获取也应该变成动态的

remove()

可以直接将选中的元素删除

最后我们将元素删除

当我们删除了选中状态的li之后,我们想要让它的前一个li处于选定状态

最好的方法不是给前面的li添加类,而是用原先写好的点击事件去触发

但是当我们删除最后一个li的时候index—会变成-1,li[index]就会报错了

可以使用 && 运算符

之后我们希望当我们删除的不是选中状态的li的时候,原来的选中状态的li保持不变

这里显然是需要一个控制条件来判断我们当前删除的li是否是被选中的,我们可以用被选中状态的class来做判断,因为当我们删除的就是被选中的li的话一旦删除文档就不可能找得到选中状态的li了,也就是说我们找不到选中状态的class名,而当我们删除的不是被选中的li,那显然删了之后还是能找到选中状态的class名的

tab栏编辑功能

平时我们双击页面的时候是会选中文字的,取消的方法就是上图的第4步

核心思路就是利用文本框,但我们双击的时候插入一个文本框,当失去鼠标焦点的时候就将文本框中的值赋值给原先的元素并且删除文本框

ondblclick()

鼠标双击事件

element.addEventListener(‘dblclick’, function(){})

双击禁止选定文字(直接复制代码就行了)

window.getSelection ? window.getSleection().removeAllRanges() : document.selection.empty()

接下来开始制作编辑功能

首先还是先添加按钮事件,之后关闭双击选中文字事件,并插入文本框

之后将原先元素的文本赋值给新添加的input文本框

之后我们想要双击之后直接选中文本框中的内容并且获得鼠标焦点

select()

element.select()

直接选中文本框中的所有文本,同时获得鼠标和键盘焦点

之后我们将文本框中的内容赋值给之前的span元素并且删掉文本框即可,注意,由于文本框是span的innerHTML,所以直接修改span的innerHTML既可以完成赋值操作,又可以删掉文本标签

之后我们希望按下回车键也能实现onblur()类似的效果

有个小技巧,既然是和blur()类似的效果,这里在触发回车事件的时候可以直接this.blur()也就相当于触发了鼠标失焦事件

blur()

element.blur()

手动调用元素失去焦点事件

最后我们给li下面对应的section添加类似的功能,好消息是section的功能完全一样,因此不需要重新写了

直接给他绑定事件就行了

构造函数和原型

构造函数

实例成员

静态成员

静态成员只能通过构造函数来访问,如果通过实例访问的话会提示undefined

构造函数的问题

由于对象是复杂数据类型,因此每开一个就要开辟一个实例空间来存放这个函数,这样会造成资源的浪费

像这种情况返回的就是false(地址不同)

构造函数原型prototype

实现方法的共享,节约内存空间

一般情况下我们把属性放到构造函数里面,公共的方法放到原型的对象身上

那么有一个问题:方法加在函数的prototype对象上,那为什么用函数生成的对象能直接使用原型上的方法呢:

那是因为有对象原型__proto__

语法

对象原型__proto__

对象原型__proto__其实就等于构造函数的prototype

对象的方法的查找规则

首先看实例对象身上是否有该方法,有就执行,如果没有由于有__proto__的存在,会去原型对象prototype身上查找该方法,找到就执行

constructor

把sing和movie两个函数分开写是一点问题都没的,但是如果写在一起了(如上图),这时候再去打印constructor:

这是因为从语法角度分析,用Star.prototype.sing = function(){}的方式添加sing这个方法不会抹去原先prototype对象中的属性和方法,而用Star.prototype = {sing: function(){}} 的写法相当于直接赋值,把原先的属性和方法都覆盖掉了,所以才会导致prototype中的constructor不再指向原构造函数

这个时候再直接输出原型prototype,发现他也被覆盖了

很显然已经不再是原来那个Star了,而是我们给他新赋值的对象

解决方法也很简单,重新给他写上就行了

构造函数、实例、原型对象三者之间的关系

原型链

prototype的__proto__与Object的__proto__

只要是个对象就会有一个__proto__

结果我们发现prototype的__proto__指向的是Object的prototype(这里可以参考原来的Star,Star.prototype === star.proto

而Object的prototype的constructor指向的是Object的构造函数

Object的prototype的__proto__指向的是null

最后得到这样一张图:

所以方法的查找路线就是沿着这条原型链一步一步上去的

Js的成员查找机制(规则)

不管是方法还是属性都是这么找的

所以继承Object的类可以使用Object的toString()方法

原型对象this指向

  1. 只有调用了才能知道这个this指向的是谁;

  2. 不管是构造函数中的this还是原型对象中的this指向的都是实例对象

原型对象的应用 扩展内置对象

当然,这个追加不能用Array.prototype = {}的方式(会报错),最佳的方法是用Array.prototype.sum = function(){}

继承

call()

这个this指向的是window

fn()效果等同fn.call()

call()还可以改变函数指向的对象,再次打印之后上面那个this指向的就不是window了,而是对象o

还能传参

继承父类属性

ES6之前的构造函数+原型对象的继承方式被称作组合继承

Son继承Father就这一句话,这里借助call()把Father中的this改成了Son的this

当然,还可以扩展Son的属性

继承父类方法

这里有个想法,我们在父亲的prototype中的写入想要继承的方法,执行 Son.prototype = Father.prototype,之后再在Son的prototype中写入自己想要的方法,这样虽然能实现,但是这个操作就等同于将Father.prototype的指针赋值给Son.prototype,会导致子做了修改会帮父一起做修改,这样显然是不行的

最终的解决方法是:

让Son的原型直接等于新的Father实例对象,由于new Father()实际上会在内存中新开辟一块空间,因此他虽然有__proto__指向Father的prototype,但却不会影响Father的Prototype,这个时候Son的prototype就是Father的一个实例对象,那显然Son是可以使用Fahter.prototype中的方法啦,而且就算在Son中扩展自己的函数,影响的也只是这个Father的实例对象,是不会影响Father的prototype的,这就完美的模拟了方法的继承

但是会有一个问题,这个时候Son的prototype的constructor指向的肯定是Father构造函数,但是实际上我们却需要它指向Son的构造函数

所以我们给它指回来

ES5中新增的方法

forEach()

filter()

some()

这里只要找到符合条件的就会直接返回true,不会再继续往下找了

map()

和foreach()类似,也是用于遍历的

every()

和some()类似,只不过every()需要数组中所有元素都满足条件才返回true

案例

1、

把数据显示到页面

封装把数据渲染到页面的函数

出现bug,应该先清空数据

接下来制作查询功能

可以用filter来做,但是由于some()找到之后就不会继续向下找的特性,some()的效率更高,所以我们用some()来做

由于some()直接拿原来的写法来写的话是返回true 或 false的,因此需要改写(如上图),我们让他找到就返回,并且特别要注意的是return 后面必须写true

思考

1、刚才的案例完全可以用forEach()或者filter()实现,那为什么不用呢?

这里会打印三次11,分别在red、blue、pink,可见forEach写了return也并不会终止循环,filter()也一样

而some()则不会再继续了

2、some()为什么一定要return true?

写了return true说明他找到了元素,return false说明没找到

some()能终止是因为这个return true,如果我们把return true改为return false,他就不会终止了:

trim()

解决了以前input表单里面字符可能为空字符的问题

keys()

Object.defineProperty()

该方法中第三个参数discriptor说明

value就是添加或者修改的属性对应的值

writable定义值是否可以重写,默认为false表示不可重写

enumerable定义属性是否能被遍历出来,默认为false

我们可以看到enumerable为false之后就遍历不出address属性了

configurable定义属性是否能被修改或者删除,默认为false

注意,当属性在之前设置了configurable为false之后,在使用Object.defineProperty()时就不能再设置该方法的第三个参数里面的特性了,比如:

我在前面设置了address属性:

如果我在后面又重新设置了一遍address属性:

是会报错的,因为第一次设置address属性的时候就设置了configurable为false

delete

删除操作

函数进阶

函数的定义和调用

以前是用这两种:

其实还有第三种方式(了解即可,不常用):

其实我们定义的每一个函数都是Function的实例

函数定义方式

函数调用方式

函数内this的指向

注意,立即执行函数 this指向的是window

改变函数内部this指向

call

apply

apply跟call基本一样,只是他的参数传递必须是数组(也可以是伪数组)

注意,他虽然传的是数组,但是到函数体里面的时候就会变成函数体参数所需要的类型,比方说我们传进去的数字型数组,进去之后会变数字型参数,字符型的数组会变字符型参数

如果他的第一个参数写的是null说明不需要改变函数this指向,但是一般不要写null,不太合适的,一般要写调用者

apply的主要应用:

比方说求数组最大值,换做以前需要用for循环去找,现在不必了:

通过apply将arr传入Math.max,即可求出最大值;min也是一样的

bind

bing和前两个的区别在于他只修改函数this指向但不会调用函数,返回的是改造过的函数的拷贝

只写fn.bind(o)是不会有输出的,因为他只返回改造后的函数的拷贝

当然bind也可以传递参数

在实际开发中bind用的最多

因为有的函数我们不需要立即调用,但是又想改变这个函数内部的this指向

上图倒数第三行不能写this.disabled,因为里面的this指向window,写btn.disabled也不太好,以前的方法是在外面声明that指向this,再写that.disabled

但是这样也很麻烦,现在我们可以用bind

由于是执行内部的function函数,因此我们的bind要加在这个函数的后面,当然,更好的写法:

bind里面的this其实就是btn

再来看这个案例

当我们循环给btn绑定事件的时候像上图这样写是有问题的,因为for循环是立即执行的,而setTimeout不是,因此当我们最后点击某个按钮的时候这里的btns[i] 里面的i并不是我们所想的是对应某个btn的索引,这个i其实是for循环遍历之后的i,这里我们btns有三个,显然i这时候已经是4了,那btns[4]显然是会报错的,因为根本没有这个btn。

解决方法:

call、apply、bind总结

严格模式

开启严格模式

为整个脚本开启严格模式

script里面最上面一行写 ‘use strict’;

有的script脚本是严格模式,有的是正常模式,为了防止合并js文件的时候导致变量污染,也可以用立即执行函数,这样独立创建一个作用域而不会影响其他的脚本文件:

之后我们把代码写到立即执行函数里面就行了,也就相当于给整个脚本开启了严格模式

为某个函数开启严格模式

直接在函数里面添加那句话就好了

严格模式中的变化

用了严格模式上面的num会报错,必须用var先声明

严格模式下删除已经声明好的变量也会报错

严格模式下this指向问题

以前全局作用域中的函数this指向的一定是window,而严格模式下它指向的是undefined

要是以前的话上图代码是可以直接写的,由于直接调用了,函数里面的this指向的是window,因此window.sex是有值的,而严格模式下由于全局的this指向undefined,我们不能给undefined赋值sex,因此在还没到console.log这一步的时候,也就是在Star()这一步的时候就会报错了,报错信息指出不能给undefined赋值sex

在严格模式下必须给构造函数加new

特殊点

不管是严格模式还是正常模式,定时器里面的this还是指向window,事件、对象还是指向调用者

严格模式下函数变化

要是以前,可以像上图这样写(这明显不合理),上图最后输出为4,那是因为变量层叠性,a=2在后面覆盖了a=1

在严格模式下不能重名

第二个就是不能再非函数的代码块内声明函数

这个也比较符合正常思维,要注意的是在函数内部还是可以声明函数的

其他的还有,像严格模式下不能使用八进制了等等

高阶函数

形参是函数或者返回值是函数都叫高阶函数

回调函数就是典型的高阶函数

加密

escape()和unescape()

这两个都是js自带的

escape()是简单加密方法

unescape()是解密方法

window.atob()与window.btoa()

这两个是base64加密解密工具

其中window.btoa()是加密用的

window.atob()是解密用的

闭包

里面的fun()函数能访问到num变量,这就是一个闭包

fun()函数能访问到别的函数(这里就是fn()函数)内的变量,这个被访问的变量所在的函数就叫闭包函数(这里显然fn()就是闭包函数,因为num变量在fn()函数内)

那么这种是函数内部的函数访问函数内部的变量,这没什么稀奇的,但其实还可以函数外部的作用域访问函数内部的局部变量!

这里我们把函数内部的函数返回出去,这样在函数外部的作用域调用函数的话就相当于借助函数内部的函数将函数内部的局部变量传递给函数外部,也就达到了在函数外部使用函数内部的局部变量

这里函数return了一个函数,所以我们的闭包也是典型的高阶函数

到这里我们能总结出闭包的主要作用:延伸了变量的作用范围

同时,原来我们讲函数里面的局部作用域会在函数执行完毕之后销毁,但是这个局部变量在函数执行完毕之后不会销毁,因为我们还有一个外部的变量等着调用它,必须等外部的变量把它调用完毕之后才会销毁(这里就是fn()函数内的局部变量num,由于外部的f还需要调用num,所以num在fn()调用完毕后还不会销毁,要等f调用完了才会销毁)

闭包案例

1、

由于for循环是同步任务,会立刻执行,而lis的click事件的function是异步任务,不会立刻执行,所以当我们回过头来点击lis(不管点哪个都一样)的时候输出的i全都是4(i++到最后为4)

以前的解决方法:

现在的解决方法:

我们利用闭包传入每次循环的i到立即执行函数,再去绑定每个li的点击事件

注意这里的闭包是立即执行函数,不是for循环

但是乍一看第二种方法更复杂了,每次循环都要创建很多函数,所以不是说闭包就一定是好的,在某些情况下它效率反而更低

而且这里的i本来在立即执行函数结束之后就可以立即释放的,但是由于里面的点击事件一直在等待点击,只有点击事件结束后i变量才会销毁,如果一直不点击就一直不会销毁,这也会造成占用内存和内存泄漏。

当然我们要看清本质,闭包的主要作用就是延伸某些局部变量的作用范围,要抓住这一点去用闭包!在实际开发中也是,需要合理利用闭包。

2、

还是上面那个例子,但是这里的需求是三秒之后打印li的内容

如果直接这么写显然是错误的,原因跟上面的一样(for循环同步任务,定时器异步任务)

正确做法:利用小闭包

3、

这里最外层的匿名的立即执行函数就是一个闭包

被延伸作用于范围的局部变量有start和total

思考题

1、

问:这里输出是什么?有没有闭包的产生?

按照上图将object.getNameFunc()()拆解

想得通的话也不用拆解

很明显这个this.name的this指向window,所以输出是“The Window”,而且我们看这一块:

最里面的函数并没有用到上一层函数的局部变量,因此它没有产生闭包

2、

还是上面那一段代码,区别是这里用了一个that

问:输出是什么?产生闭包了吗?

这里还是拆解一下:

谁调用函数this就指向谁

所以这一块:

这个that指向的其实是object,所以可以判断输出是“My Object”

那有没有用到闭包呢?

答案是有的!

因为他用到了that这个变量,而that是最内层的函数的上一层函数的局部变量,所以是有产生闭包的。

而且闭包就是getNameFunc这个函数

闭包总结

还是要强调一下,that这个局部变量在getNameFunc()执行完毕之后(上图的箭头)不会立即销毁,他会等其他函数调用完之后才销毁。

process对象

http://javascript.ruanyifeng.com/nodejs/process.html

递归

在递归中使用forEach的话还需要比一般的for循环多写一步:

通过测试,js的forEach里面直接return是没用的,需要通过在外层设置全局变量传参的方式才行,这点需要注意

浅拷贝

浅拷贝如果拷贝的是对象级别的,那他只是拷贝了地址

Object.assign(target, source)

实际开发中浅拷贝一般直接用assign,不会去写for循环

深拷贝

注意这里Array的判断写在Object前面,因为Array本身也是Object

正则表达式

创建正则表达式

测试字符串是否符合正则表达式

符合则返回true,不符合返回false

量词符

要注意的是 ? ,表示的是出现0次或者1次

小括号

表示优先级

不加小括号就是匹配abccc

加了小括号就是匹配abcabcabc

或者符号 |

中文匹配

\u 表示Unicode编码

这个范围包含了所有中文字符

侵占模式 (?>Pattern)

和侵占量词 “+” 可以通用

模式修正符

替换replace

原先是直接替换具体文本,其实还可以写正则表达式。

但是像图上这样还是会有问题:

全局匹配与忽略大小写

典型易错案例

1、

这里要求以a或b或c开头或者结尾,但是只能存在一个字符,所以除了“a”、“b”、“c”这三个是true,其他的都是false

2、

特别注意!

写{6,16}的时候逗号后面不能有空格!!!不然就会失效

而且{6,16}的意思是大于等于6,小于等于16

案例

典型易错案例!

当我们循环给btn绑定事件的时候像上图这样写是有问题的,因为for循环是立即执行的,而setTimeout不是,因此当我们最后点击某个按钮的时候这里的btns[i] 里面的i并不是我们所想的是对应某个btn的索引,这个i其实是for循环遍历之后的i,这里我们btns有三个,显然i这时候已经是4了,那btns[4]显然是会报错的,因为根本没有这个btn。解决方法:

技巧

1、由于some()找到之后就不会继续向下找的特性,就查找单个唯一元素来讲some()比filter()更合适