文章目录
- 写在前面
- 构造函数
- new的时候做了哪些什么事情呢?
- 实例成员和静态成员
- 构造函数存在的问题
- 实例对象的原型, **__proto__**
- 方法查找规则
- constructor 作用一 传递参数和自执行(类中)
- constructor作用二,将引用的构造函数指回原来的 例子c-6
- 构造函数的“继承”
- 总结
- 原型和原型链
- 原型链
- 成员查找机制
- 类和对象
- 面向对象的特性
- 创建类和实例对象
- constructor
- 类的继承
- super 关键字
- 总结
- 闭包以及闭包存在的问题
- this的指向问题
- 函数的定义方式 例子 c-z
- 函数的调用方式 例子c-f
- Object.defineProperty
- 浅拷贝和深拷贝
- js的高阶函数
- 递归
- ES6新特性
- 剩余参数
- 扩展运算符
- Array.from
- Array.find
- Array.every
- Array.some
- Array.findIndex
- Array.includes
- startsWith() 和endWith()
- repeat
- set
- 利用set进行数组的去重
- 遍历set
- 区别 let const var
- let
- const
- var
- 解构赋值操作
- 箭头函数
- 模板字符串
- 写到最后
写在前面
这篇文章总结一下js中高级中存在的一些易错和易混的概念型的问题,主要包含了一下一个模块
- 构造函数
- 原型和原型链
- 类
- 闭包以及闭包存在的问题
- this的指向问题
- Object.defineProperty
- 浅拷贝和深拷贝
- js的高阶函数
- 递归
- ES6中let const var
- ES6中的解构赋值操作
- ES6箭头函数
- ES6模板字符串
每一个知识点我都会尽可能的讲明白,写一些Demo给你们,就像前面写canvas的时候一样,尽量多写一些有说明性的代码,两个目的,第一个是总结一下,第二个是进行一个记录,也给学习js的过程中比较迷茫的一些提示,这篇文章是属于js中相对中高级的,所以初级的看起来会有一些困难,但是初级的可以直接百度或者看我之前的一些关于js的文章进行学习也是可以的!虽然上面的每一个点都是可以直接单独拿出来写一篇文章的,我也不是没这么计划,只是觉得这样会显的这个知识点好像很难一样,会劝退一部分人,所以就直接一篇文章直接搞定算了,篇幅会比较长,和前面的小程序和canvas一样,因为最近都是写一些总结性质的文章,可能会相对比较繁琐,读的时候可以直接收藏,后面慢慢看!我也会尽量的将每一个模块都分的比较清楚!
构造函数
代码语言:javascript复制概念解释:构造函数也是一种js函数,只是他比较特殊,第一它的作用比较特殊,它主要是初始化某一种对象(某一类),将成员变量的一些公共属性封装到函数中,特殊二在于它总是和new一起使用,也就是如果它没有进行和new配合使用,那么这个构造函数是没有意义的,一般情况下我们为了区分普通函数和构造函数,在声明构造函数的时候首字母都是大写! 在面向对象编程的语言中基本上都有类的概念,但是js本身却没有类的概念,但是js也是面向对象编程的,他的替代方案就是构造函数,构造函数的作用其实就是变相的类,他具有类的基本上所有特性,下面我们声明一个最简单的构造函数 例子c-1
//声明一个最简单的构造函数,动物类 c-1
function Ani(aName,aGender){
this.aName = aName
this.aGender = aGender
this.eatFood = function(){
console.log(`动物会吃饭`)
}
}
let animal = new Ani('狗','公')
animal.eatFood()
console.log(animal)
//动物会吃饭
//Ani { aName: '狗', aGender: '公', eatFood: [Function] }
new的时候做了哪些什么事情呢?
- 在内存中初始化了一个空对象
- 将this指向这个空对象
- 执行构造函数里面的代码,将构造函数中的内容赋值给空对象
- 返回这个实例对象本身
实例成员和静态成员
- 实例成员顾名思义指的是实例对象可以直接调的成员(或者理解为this指向的成员),比如上面的aName、aGender、earFood(),这些成员只能通过实例对象进行调用,构造函数本身是不可以进行访问的 【Ani.aName】是错误的
animal.eatFood()
animal.aName
animal.aGender
- 静态成员是直接通过构造函数本身进行添加的成员
Ani.age = 12 //age就是静态成员,直接通过构造函数进行添加,同时只能通过构造函数进行访问,实例对象是不可以访问的
console.log(Ani.age)
构造函数存在的问题
- 内存浪费和解决方案
代码语言:javascript复制如果构造函数里面没有引用数据类型全部都是基本数据类型的话,是不会造成内存浪费的情况的,说简单一些,因为引用数据类型在被使用的时候,系统会重新添加一块内存空间出来,为了解决这个问题,一般引用数据类型或者说是公共方法建议是直接写到构造函数原型上进行共享内存地址。通过原型分配的函数是所有成员共享的,所谓的原型prototype,是每一个构造函数都有的,这里不理解的话,可以认为他们就是一家的,有构造函数就一定有prototype,prototype本身就是一个对象,只是他的属性和方法都被构造函数所拥有 例子 c-2
// c-2
function Ani(aName,aGender){
this.aName = aName
this.aGender = aGender
this.eatFood = function(...foodType){
console.log(`吃${foodType}`)
}
}
let dog = new Ani()
let cat = new Ani()
dog.eatFood('骨头','肉') //吃骨头
cat.eatFood('小鱼干') //吃小鱼干
// 这里的吃东西这个函数其实不管是哪一种动物进行调用,都是做一件事,就是吃,但是两个不同的实例对象调用的时候,内存声明了两次,这里就造成了内存浪费,将吃这个函数写到原型上,进行内存地址的共享,也就是不会再开辟内存空间
Ani.prototype.eatOther = function(...FT){
console.log(`吃${FT}`)
}
dog.eatOther('骨头','肉') //吃骨头
cat.eatOther('小鱼干') //吃小鱼干
// 证明:查看两次函数地址是否相同
console.log(dog.eatFood === cat.eatFood) //false
console.log(dog.eatOther === cat.eatOther) //true
实例对象的原型, proto
代码语言:javascript复制通过上面的例子,我们可以看到,dog和cat都可以直接访问eatOther的方法,但是我们的eatOther的方法明明是直接被原型对象分配的,并不是构造函数内部的属性,为什么我们的实例对象可以访问呢?这里有两种解释,但是都是对的,第一种是通过原型对象分配的属性和方法构造函数都拥有,第二种解释其实就是对第一种的延伸,为什么拥有呢?因为每一个实例对象上都有一个proto原型的存在,这个proto就是指向了prototype了,所以实例对象可以直接访问原型对象上的属性和方法,例子c-3
// c-3 __proto__ 演示
function Ani(aName, aGender) {
this.aName = aName
this.aGender = aGender
}
Ani.prototype.eatOther = function (...FT) {
console.log(`吃${FT}`)
}
let dog = new Ani('狗', '公')
// 证明:实例对象的__proto__ 指向了 原型对象 如下:
console.log(dog.__proto__ === Ani.prototype) //true
console.dir(dog)
/**
aGender: "公"
aName: "狗"
[[Prototype]]: Object
eatOther: ƒ (...FT)
constructor: ƒ Ani(aName, aGender)
[[Prototype]]: Object
constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toLocaleString: ƒ toLocaleString()
toString: ƒ toString()
valueOf: ƒ valueOf()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
__proto__: Object
eatOther: ƒ (...FT)
constructor: ƒ Ani(aName, aGender)
[[Prototype]]: Object
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()
*/
方法查找规则
- 在实例对象本身查找
- 实例对象本身不存在,就查找实例对象原型__proto__ 上构造函数原型对象上是否存在 例c-4
//方法查找规则演示 c-4s
function Ani(aName, aGender) {
this.aName = aName
this.aGender = aGender
}
Ani.prototype.eatOther = function (...FT) {
console.log(`吃${FT}`)
}
let dog = new Ani()
//如果dog本身有该方法,就不会去找原型对象上的方法
dog.eatOther = function (...FT) {
console.log(`不吃${FT}`)
}
dog.eatOther('小鱼干')
// 不吃小鱼干
constructor 作用一 传递参数和自执行(类中)
代码语言:javascript复制实例对象原型__proto__ 和原型对象prototype里面都有一个属性,叫做constructor,他也叫做构造函数,因为它本身指向的就是构造函数本身,如果有心注意的话,例子c-3的打印信息里面就有constructor的出现,他的**目的就是为了指明当前的引用了哪一个构造函数 **例子 c-5
//证明:contructor指向的就是就是构造函数本身 c-5
function Ani(aName, aGender) {
this.aName = aName
this.aGender = aGender
}
let dog = new Ani()
console.log(Ani === Ani.prototype.constructor) //true
console.log(Ani === dog.__proto__.constructor) //true
constructor作用二,将引用的构造函数指回原来的 例子c-6
代码语言:javascript复制//c-6
//基础写法
function Ani(aName, aGender) {
this.aName = aName
this.aGender = aGender
}
Ani.prototype.eat = function () {
console.log("吃")
}
Ani.prototype.move = function () {
console.log("移动")
}
let dog = new Ani()
console.log(Ani.prototype.constructor)
//打印信息:可以看出,当前的constructor 是指向Ani这个构造函数的
// Ani(aName, aGender) {
// this.aName = aName
// this.aGender = aGender
// }
// 改一种写法
Ani.prototype = {
eat:function(){
console.log("吃")
},
move:function(){
console.log("移动")
}
}
console.log(Ani.prototype.constructor)
//打印信息:可以看到,当前的constructor 已经不是指向Ani了
//Object() { [native code] }
//将引用构造函数重新指向Ani
Ani.prototype = {
//将引用构造函数重新指向Ani,需要将Ani重新给到constructor
constructor:Ani,
eat:function(){
console.log("吃")
},
move:function(){
console.log("移动")
}
}
console.log(Ani.prototype.constructor)
//打印信息
// Ani(aName, aGender) {
// this.aName = aName
// this.aGender = aGender
// }
构造函数的“继承”
代码语言:javascript复制这里我为什么加上双引号呢?因为构造函数本身是没有继承的,最起码在es6之前是没有的,但是js通过构造函数加上原型对象的方式实现了继承的一些特性,所以一定意义上也可以叫做继承,首先要搞明白什么是继承, 可以直接跳到类的继承介绍,类的继承有extends关键字,但是构造函数可没有,但是js提供了call的方法可以将this的指向改变,【this的指向改变可以移步到下文this的指向问题】,那么我们就可以通过call的方式将子构造函数的指向改为父构造函数的this,那么就实现了伪继承 例子:c-c
//c-cs
function F(x, y) {
this.x = x
this.y = y
}
function C(x, y) {
//将this指向父构造函数
F.call(this, x, y)
}
let c = new C(1, 2)
console.log(c) //C { x: 1, y: 2 }
- 方法的继承
function F(x, y) {
this.x = x
this.y = y
}
F.prototype.move = function(){
console.log("我会走")
}
function C(x, y) {
F.call(this, x, y)
}
// 不可以直接将 C.prototype = F.prototype 因为这样会导致子构造函数添加方法的时候也给父构造函数添加一个方法
C.prototype = new F()
let c = new C(1, 2)
c.move() //我会走
总结
可能第一个知识点就劝退了一部分人,我承认我写的是比较枯燥的,但是学习这个东西本身就是很枯燥的,这里总结一下构造函数,实例,原型对象三者之间的关系,首先构造函数有一个prototype的属性指向了原型对象,原型对象上存在一个constructor指向了构造函数,实例的原型__proto__指向了原型对象,又因为原型对象指向了构造函数,所以__proto__也指向了构造函数,有点绕,但是理解了 ,就比较容易了!关于构造函数我们就说这么多!
原型和原型链
原型链
代码语言:javascript复制原型链是一个比较难以理解的东西,但是只要上面的构造函数搞明白了,这个原型链应该也问题不大,下面我们简单的说一下什么是原型链条,这个难为很多人的知识点,例子c-7 实例对象的proto的原型指向的是原型对象prototype,原型对象prototype的proto原型指向的是Object的prototype,Object.prototype.proto原型指向的是null,这样的一条原型指向就叫做原型链
//c-7 原型链演示
function Ani(aName, aGender) {
this.aName = aName
this.aGender = aGender
}
let dog = new Ani()
// 原型对象上的__proto__ 指向了Object的原型对象,
console.log(dog.__proto__.__proto__)
console.log(Ani.prototype.__proto__)
console.log(Object.prototype)
console.log(Object.prototype.__proto__)
//实例对象的__proto__的原型指向的是原型对象prototype,原型对象prototype的__proto__原型指向的是Object的prototype,Object.prototype.__proto__原型指向的都是null,这样的一条原型指向的就叫做原型链
console.log(dog.__proto__.__proto__ === Ani.prototype.__proto__) //true
console.log(Ani.prototype.__proto__ === Object.prototype) //true
成员查找机制
上面通过一段代码展示了什么是原型链条,他的作用是什么呢?原型链的一个主要作用就是进行属性方法的查找,当访问一个实例对象的属性时,是按照下面的顺序进行查找的
- 首先通过查找实例对象本身有没有该属性
- 其次查找他的原型__proto__有没有该属性【其实就是原型对象有没有该属性,因为__proto__指向的是原型对象】
- 然后查找原型对象的原型上有没有,也就是Object上是不是存在
- 最后一直找到null为止
例子c-8
代码语言:javascript复制// c-8
function Ani(aName, aGender) {
this.aName = aName
this.aGender = aGender
}
let dog = new Ani()
dog.name = '狗'
console.log(dog.name) // 狗
Ani.prototype.pName = '小狗'
console.log(dog.pName) //小狗
Object.prototype.oName = '金毛'
console.log(dog.oName)//金毛
console.log(dog.nName) //undefined
类和对象
在说类之前,先说两种编程思想,一个是面向过程,一个是面向对象,这个估计已经被很多人讲的不能再讲了,不管哪一种计算机语言出现,都会先说明是面向过程还是面向对象,两种谁好谁坏倒没有特别明显的差异,只是两种不同的解决问题的思路而已,面向过程讲究的解决问题的步骤,举个例子,面向过程就好像你吃一份蛋炒饭,面向对象就好像你吃一份盖浇面,区别是蛋炒饭很好吃,但是你想将蛋炒饭里面不要蛋,就比较麻烦, 盖浇面的好处在于,盖头个面是分开的,你想吃什么口味的都可以,这样的例子不知道会不会有助于你更好地理解这两种不同的思想,下面我们的说的类,就是一种面向对象的思想,也就是将不同的功能模块进行封装,最后通过不同的组合来实现不同的功能!面向对象的优势在于灵活,面向过程的优势在于性能比较好,类也是es6之后提出来,但是因为需要讲的比较多,所以就单独拉出来讲一下!类本身也是一种构造函数 对象:在js中万物皆是对象,比如方法,变量,方法等等 类和对象的联系:类是某一个大类,比如动物,对象是指类的某一个典型具体的,比如狗,猫等 例子c-9
面向对象的特性
- 封装性
- 继承性
- 多态性
创建类和实例对象
代码语言:javascript复制//c-9
//创建一个动物类
class Ani {}
//创建一个实例对象
let d = new Ani()
constructor
代码语言:javascript复制构造函数部分已经讲过constructor,在类里面,constructor也叫做构造函数,他的作用在于传递参数和返回实例对象,通过new的时候自动执行constructor里的代码,如果我们不写constructor的话,类内部会自动帮我们创建一个constructor 例子c-10
//c-10
class Ani {
constructor(aName, aGender) {
this.aName = aName
this.aGender = aGender
this.eatFood()
}
eatFood(){
console.log('我在new的时候被执行了')
}
}
new Ani() //我在new的时候被执行了
类的继承
代码语言:javascript复制类和类 之间是可以进行继承,子类继承了父类之后,也就继承他的所有属性和方法,也就是说,父类的方法和属性都可以被子类给复用 例子 c-11
//c-11
class Ani {
constructor(aName, aGender) {
this.aName = aName
this.aGender = aGender
}
eatFood() {
console.log('我是继承过来的方法')
}
}
class Dog extends Ani {}
let d = new Dog('金毛')
console.log(d.aName) //金毛
d.eatFood() //我是继承过来的方法
super 关键字
代码语言:javascript复制这里讲一下super的使用,在子类继承了父类之后,因为每一个类都有自己的constructor构造函数,也都有自己的形参,那么当父类里面存在使用自己的参数进行处理操作的时候,那么子类直接调用父类的方法就会出现报错的现象,也就是this的指向问题,例子:c-12
//c-12
class F {
constructor(m, n) {
this.m = m
this.n = n
}
productNum() {
console.log(this.m * this.n)
}
}
class C extends F {
constructor(x, y) {
this.x = x
this.y = y
}
}
let c = new C(1, 2)
c.productNum()
// 报错:Must call super constructor in derived class before accessing 'this' or returning from derived constructor
问题就出现在了我们传递参数的时候,constructor本身是当前类的构造函数,所以我们是没有办法直接调用到父类的productNum方法的,这个时候我们需要告诉当前的子类,我们传递的实参是给父类的,而不是自己要用的 例子:c-13
- super调用父类的构造函数
// c-13
class F {
constructor(m, n) {
this.m = m
this.n = n
}
productNum() {
console.log(this.m * this.n)
}
}
class C extends F {
constructor(x, y) {
//调用父类的构造函数
super(x, y)
}
}
let c = new C(1, 2)
c.productNum() // 2
- super调用父类的普通函数 在使用super 的时候我们一般都是使用它调用父类的构造函数,也就是将形参的传递方向改变,那么其实super也可以调用父类的普通函数,比如例子14
// c-14
class F {
ordinaryFunction() {
console.log("我是F类")
}
}
class C extends F {
ordinaryFunction() {
console.log("我是C类")
}
}
let c = new C()
c.ordinaryFunction() // 我是C类
//类继承之后方法查找顺序:实例对象查找方法先在本身查找,找不到才会去父类查找
调用父类的普通函数 类继承之后方法查找顺序:实例对象查找方法先在本身查找,找不到才会去父类查找
代码语言:javascript复制// c-14
class F {
ordinaryFunction() {
console.log("我是F类")
}
}
class C extends F {
ordinaryFunction() {
super.ordinaryFunction()
}
}
let c = new C()
c.ordinaryFunction() // 我是F类
- super使用之后,还要执行自定义的方法
代码语言:javascript复制子类继承了父类的方法之后,很多时候我们都是需要进行有自定义的函数处理一些业务的,这个时候我们可以将super和本身自己的形参同时写上 比如例子c-15 需要注意的是 super必须写到本身的变量赋值的前面
// c-15
class F {
constructor(m, n) {
this.m = m
this.n = n
}
add() {
console.log(this.m this.n)
}
}
class C extends F {
constructor(x, y) {
super(x, y)
this.x = x
this.y = y
}
minus() {
console.log(this.x - this.y)
}
}
let c = new C(1, 2)
c.add() //3
c.minus() //-1
- 注意的点:
- 类是没有变量提升的,也就是说必须先定义一个类,才可以进行实例话
- 类里面的属性和方法必须使用this指向才可以
- 类中的this的指向问题 constructor里面的this指向是实例对象 方法里面的this指向的是调用者。例子c-16
//c-16s
let that,_this
class F {
constructor(m, n) {
this.m = m
this.n = n
that = this
}
add() {
_this = this
}
}
let f = new F(1, 2)
console.log(f === that) //true
f.add()
console.log(f === _this) //true
总结
至此关于this指向的问题就写完了,这里因为考虑到篇幅和我个人的能力问题,不去写具体一些比较偏门的this指向的问题,这些基本上是足够我们日常编程使用了,关于上面的this指向为什么没有验证,两个原因,验证的过程需要截图,本身篇幅就已经很长了,图片会显示的更长, 第二是 这种验证你们自己去实际操作一下比较直观也比较容易记得住,所以我这里就不一一验证了,但是上面的代码都是自己验证过的,都是没有问题的,有任何问题可以直接私信我或者下方留言!
闭包以及闭包存在的问题
代码语言:javascript复制在说闭包之前首先要明白的一个点是作用域的概念,在js中函数内部的变量只能被函数内部调用,外部是不可以调用的,内部的也被成为局部变量,函数执行结束之后,这个变量也会被销毁,这个我在js的垃圾回收机制里面说过,感兴趣的可以去阅读一下,闭包就是打破了这一常规的作用域的概念,他可以使一个函数内部访问另一个函数内部的变量,这种现象或者这样的函数就叫做闭包 例子 c-b
//c-b
function bb() {
let name = "jim"
return function() {
console.log(name)
}
}
bb()()
//jim
//或者
function bb() {
let name = "jim"
function nb() {
console.log(name)
}
nb()
}
bb()
//jim
- 如何验证是否产生闭包 在google浏览器控制台进行bb函数的断点,执行下一步的时候,执行到内部的nb的时候,右侧会出现闭包的函数提示,如上bb就是一个闭包函数或者bb函数就产生了闭包的现象
- 闭包是如何产生内存泄漏问题的
面试的时候经常会问到的一个问题就会闭包会产生内存泄漏的问题,首先这个问题要搞明白的一个点是什么叫做内存泄漏,在js中内存泄漏指的是程序运行结束,未使用的或者已经被使用过的变量应该得以销毁却没有被销毁的现象就叫做内存泄漏,那么根据我前面关于js内存清除机制可以知道,js是由一个root节点进行查找该变量有没有被使用来判断他是不是需要销毁的,普通函数执行结束就不会被root节点找到,所以会被销毁,但是闭包因为是内部调用,所以js在查找的时候找到内部函数还在调用这个变量,那么这个时候他不会主动销毁这个变量,所以就导致了内存泄漏!
this的指向问题
this的指向问题一只都是一个比较头疼的问题,因为涉及到的的情况比较多,其实我自己也不敢保证所有的情况我都是很确定的,很多时候我也是写一个demo自己尝试一下,看看this的指向到底是什么,所以下面的例子是我写的过程中觉得比较有意义的一些,希望可以帮助大家理解这一块的内容 一般的原则是 this指向的都是调用者
函数的定义方式 例子 c-z
代码语言:javascript复制在没有进行说this指向问题之前,首先要搞明白的事情是,函数的多种定义方式,因为不同的定义方式意味着this的指向是不同的,下面罗列几种函数常见的定义方式
// 自定义函数 c-z
function f(){}
// 函数表达式 匿名函数
let f = function(){}
// new Function
let n = new Function()
// 所有的函数都是Funtion的实例对象
函数的调用方式 例子c-f
代码语言:javascript复制//c-f
function f(){}
f() //函数名字执行 this指向的是window
let o = {f:function(){}}
o.f() //对象引用执行 this指向的是o
function F(){}
new F() //实例化的时候执行 this 指向的是实例对象
element.onclick = function(){} //元素点击执行 this指向的是element
setInterval(()=>{},1000) //一秒后执行 this指向的是window
(()=>{})() //立即执行 this指向的是window
- this指向问题 根据上面的调用方式进行分别说明
- 普通函数的时候 this指向的window
- 构造函数的时候 this执行的是实例对象
- 对象方法调用 this指向的是该对象
- 事件元素绑定 this指向的是该元素
- 定时器和立即执行函数 this指向的都是window
- 构造函数中 this指向的是实例对象 例c-17
//c-17
let that
function F(){
that = this
}
let f = new F()
console.log(f === that) // true
- 原型对象中的方法 this指向的是调用者,其实这个和类是保持一致的,可以看c-16的例子
let _this
function F() {}
F.prototype.move = function () {
_this = this
}
let f = new F()
f.move()
console.log(f === _this) //true
- 使用call、apply、bind改变this的指向
call
代码语言:javascript复制call作用有两个 第一:可以直接调用函数并且执行 第二 改变this的指向 call也可以传递参数例子c-t
//c-t
function f(x, y) {
console.log(x, y)
console.log(this)
}
f()
//Window {window: Window, self: Window, document: document, name: '', location: Location, …}
let o = {
name: 'jim'
}
f.call(o, 1, 3)
//1 3
// {name: 'jim'}
apply
代码语言:javascript复制call作用有两个 第一:可以直接调用函数并且执行 第二 改变this的指向 call也可以传递参数 和call区别在于他的参数必须是一个数组 例子c-a
//c-a
function f(x, y) {
console.log(x, y)
console.log(this)
}
let o = {
name: 'jim'
}
f.apply(o, [1,2]) //这里和call的区别在于他的参数必须是一个数组
//1 2
// {name: 'jim'}
Bind
代码语言:javascript复制bind也可以进行改变this的指向,但是他不会立即执行函数,不过他会将改变this指向之后的函数返回回去,他的参数也是string类型 例子c-b
//c-b
function f(x, y) {
console.log(x, y)
console.log(this)
}
let o = {
name: 'jim'
}
let newf = f.bind(o, 1, 2)
newf()
//1 2
// {name: 'jim'}
- 举例子
let btn = document.getElementById('btn')
btn.onclick = function () {
console.log(this) //指向的是btn
setTimeout(function () {
console.log(this) //指向的是btn(原本指向的是window)
}.bind(btn), 3000)
}
Object.defineProperty
这个东西如果你会vue的话,应该是不会太陌生,因为vue的双向绑定的机制就是通过这个属性实现的,这个属性就是定义新属性或者修改原有属性的方法 c-o
- 常规的修改对象的方法
//c-o
let o = {
name: "karry",
age: 18,
height: '180cm'
}
o.score = 100
o.age = 20
console.log(o) //{ name: 'karry', age: 20, height: '180cm', score: 100 }
- 使用Object.defineProperty进行修改 c-u
//c-u
let o = {
name: "karry",
age: 18,
height: '180cm'
}
Object.defineProperty(o,'age',{
value : 22,// 当前的值
writable : true, //是否可以被重写 默认false
enumerable : false, //是否可以被遍历 默认false 所以下面展示出来的没有value的值
configurable : true, //是否可以被再次修改或者删除 默认false
})
console.log(o)
//{ name: 'karry', height: '180cm' }
这里不做具体的讲解,自己试试就好了,没有什么难以理解的,最好的方式是自己每一个属性都试试
- Get set 例子c-s
//c-s
let o = {
name: "karry",
age: 18,
height: '180cm'
}
let { name, age, height } = o
Object.defineProperty(o, 'age', {
set(nAge) {
console.log(`我的名字是${name},我的年龄是${nAge}`)
},
get() {
return 20
}
})
o.age = 19 //我的名字是karry,我的年龄是19
console.log(o.age) //20
- 给每一个属性都添加一个get set方法 也就是vue里面的data函数中return出去的被监听的值 c-v
//c-v
let o = {
name: "karry",
age: 18,
height: '180cm'
}
for (let i in o) {
Object.defineProperty(o, i, {
set(i) {
console.log(`我的属性是${i}`)
},
get() {
console.log(this)
}
})
}
o.name = 'jim'
o.name
//我的属性是jim
//{
// name: [Getter/Setter],
// age: [Getter/Setter],
// height: [Getter/Setter]
//}
浅拷贝和深拷贝
- 浅拷贝
浅拷贝常见的方法:例子c-c1
- 直接赋值
- for遍历
// 直接赋值 c-c1
let o = {
name: "jim",
age: 18,
child: {
name: 'kim'
}
}
let k = {}
k = o
//{ name: 'jim', age: 18, child: { name: 'kim' } }
//for遍历
for (let i in o) {
k[i] = o[i]
}
console.log(k) // 使用这种方案的话,是没办法进行Symbol属性的拷贝的
//{ name: 'jim', age: 18, child: { name: 'kim' } }
//使用扩展运算符
k = {...o}
//使用assign
k = Object.assign({},o)
//数组的浅拷贝
let arr = [1,2,4,[4,5,6],8]
//使用扩展运算符
let nArr = []
nArr = [...arr]
//使用concat
nArr = arr.concat([])
//使用slice
nArr = arr.slice()
- 浅拷贝存在的问题
代码语言:javascript复制浅拷贝对于拷贝原对象来说其实就是一个地址,基本数据类型没有新地址,引用数据类型存在新的地址,那么其实我们拷贝了一个地址的话,就意味着拷贝的对象改变会导致原对象也进行变化 例子:c-w
//c-w
let o = {
name: "jim",
age: 18,
child: {
name: 'kim'
}
}
let k = {}
for (let i in o) {
k[i] = o[i]
}
console.log(k) //{ name: 'jim', age: 18, child: { name: 'kim' } }
k.name = 'marry' //基本数据类型过不会变化
k.child.name = 'karry' //引用数据类型会导致原的对象发生改变
console.log(o) //{ name: 'jim', age: 18, child: { name: 'karry' } }
- 深拷贝
深拷贝会将值拷贝过去,会申请一块新的内存,不会影响之前的对象数据,最常见的是使用JSON进行直接深拷贝 例子c-s2
- JSON.parse(JSON.stringify(o))
//c-s2
let o = {
name: "jim",
age: 18,
child: {
name: 'kim'
}
}
let k = {}
k = JSON.parse(JSON.stringify(o))
console.log(k) // { name: 'jim', age: 18, child: { name: 'kim' } }
k.child.name = 'karry'
console.log(o) // { name: 'jim', age: 18, child: { name: 'kim' } }
console.log(k) //{ name: 'jim', age: 18, child: { name: 'karry' } }
- 递归进行深拷贝 例子c-d1
//c-d1
/**
*
* @param {*} n 新拷贝的对象
* @param {*} o 之前的拷贝对象
*/
let deepC = function (n, o) {
for (let i in o) {
let currItem = o[i]
if (currItem instanceof Array) {
//如果是数组的话,直接将当前第i项进行清空,然后将当前的旧的对象的数据给到新值
n[i] = []
deepC(n[i], currItem)
} else if (currItem instanceof Object) {
n[i] = {}
deepC(n[i], currItem)
} else {
n[i] = currItem
}
}
}
deepC(k, o)
console.log(k)
// { name: 'jim', age: 18, child: { name: 'kim' }, arr: [ 1, 2, 3 ] }
注意:这里的Array属于Object 所以这里的判断条件不可以写反,否则会导致拷贝不彻底
js的高阶函数
高阶函数其实我们经常使用,只是我们使用的过程中不知道他是一个高阶函数而已,高阶函数满足两个条件中的任意一个都可以,第一个是 他的形参数是一个函数,第二个是他的返回值是一个函数 例子 c-g1 和c-g2s
- 参数是函数
//c-g1
function gf(callback){
callback && callback()
}
let f = function(){
console.log("this is function")
}
gf(f())
//this is function
- 返回值是函数
// c-g2
function gf(){
return function(){
console.log("this is function")
}()
}
gf()
//this is function
递归
递归指的是函数本身自己调用自己的过程就叫做递归,这种写法的函数就叫做递归函数
- 计算阶乘
代码语言:javascript复制递归一般都是举这个例子,我们也写一下,所谓的阶乘就是 1234567…进程乘积,给定一个实参,进行计算参数以内的乘积 例子 c-d
//c-d
let jc = function (n) {
if (n === 1) {
return 1
}
return n * jc(n - 1)
}
console.log(jc(10)) //3628800
- 计算tree结构的数据 例子 c-t
//c-t
let data = [
{
id: 1,
name: '手机',
child: [
{
id: 1 - 1,
name: '苹果',
child: [
{
id: 1 - 1 - 1,
name: '4S'
}
]
}
]
},
{
id: 2,
name: '电脑'
}
]
var rO = {}
let recursion = function (ary, id) {
for (let i of ary) {
if (i.id === id) {
rO = i
} else if (i.child && i.child.length > 0) {
rO = recursion(i.child, id)
}
}
return rO
}
console.log(recursion(data, 1 - 1 - 1)) //{ id: -1, name: '4S' }
// 写法二
const getArea = (json, value) => {
let names = []
let codes = []
function findCity(list, value, father) {
const index = list.findIndex(ev => ev.value == value)
if (index > -1) {
const child = list[index]
names.unshift(child.text)
codes.unshift(child.value)
father && names.unshift(father.text) && codes.unshift(father.value)
father && findCity(json || [], father.value)
return
}
list.map(item => {
if (item.children) {
findCity(item.children || [], value, item)
}
})
}
findCity(json, value)
names = [...new Set(names)]
codes = [...new Set(codes)]
return { codes, names }
}
ES6新特性
剩余参数
代码语言:javascript复制当实参大于形参数个数的时候,我们可以使用剩余参数的写法进行函数的定义 例子c-a1
//c-a1
let fn = (arg, ...args) => {
console.log(arg) //1
console.log(args) //[ 2, 3, 4, 5 ]
}
fn(1, 2, 3, 4, 5)
- 配合解构函数一起使用,当我们不知道具体的个数的时候,可以直接进行解构 例子c-a2
//c-a2
let r = [1, 2, 3, 4, 5]
let [...args] = r
console.log(args) //[ 1, 2, 3, 4, 5 ]
let or = [
{ id: 1, name: 'jim' },
{ id: 2, name: 'tom' }
]
let [name1,name2] = or
console.log(name1,name2) //{ id: 1, name: 'jim' } { id: 2, name: 'tom' }
let [...names] = or
console.log(names) //[ { id: 1, name: 'jim' }, { id: 2, name: 'tom' } ]
扩展运算符
> 扩展运算符是将数组拆分为以逗号分隔的序列 例子c-a3
代码语言:javascript复制//c-a3
let r = [1, 2, 3, 4, 5]
console.log(...r) //1 2 3 4 5
let or = [
{ id: 1, name: 'jim' },
{ id: 2, name: 'tom' }
]
console.log(...or) //{ id: 1, name: 'jim' } { id: 2, name: 'tom' }
Array.from
代码语言:javascript复制将伪数组(可遍历对象)改为真数组 例子 c-a4
//c-a4
let r = {
'0': '1',
'1': '2',
'2': '3',
length : 3
}
let nr = Array.from(r)
console.log(nr)
Array.find
代码语言:javascript复制查找符合条件的第一个数据,只会找到第一个 就不会再继续查找了 例子 c-a5
//c-a5
let fo = [
{
id: 1,
name: 'jim'
},
{
id: 2,
name: 'kim'
},
{
id: 3,
name: 'tom'
},
{
id: 4,
name: 'jim'
}
]
let ro = fo.find((item) => item.name === 'jim')
console.log(ro) //{ id: 1, name: 'jim' }
Array.every
代码语言:javascript复制查找数组中是不是每一项都符合 返回的是一个boolean 例子 c-a6
//c-a6
let eo = fo.every((item)=> item.name === 'jim')
console.log(eo) //false
Array.some
代码语言:javascript复制查找数组中是不是存在一项 返回的是一个boolean. 例子c-a7
//c-a7
let eo = fo.some((item)=> item.name === 'jim')
console.log(eo) //true
Array.findIndex
代码语言:javascript复制查找数组中符合条件的第一个值的索引 例子c-a8
//c-a8
let eo = fo.findIndex((item)=> item.name === 'jim')
console.log(eo) //0
Array.includes
代码语言:javascript复制查看数组是否包含某一项 例子c-a9
//c-a9
let no = [1,23,4,4]
let r = no.includes(4)
console.log(r) //true
startsWith() 和endWith()
代码语言:javascript复制字符串是否以**开头或者结尾 例子c-10
//c-a10
let str = 'this is clearlove blog'
let s = str.startsWith('this')
console.log(s) //true
let e = str.endsWith('blog')
console.log(e) //true
repeat
代码语言:javascript复制将字符串复制几份 例子c-a11
//c-a11
let str = 'clearlove blog'
let c = str.repeat(4)
console.log(c) //clearlove blogclearlove blogclearlove blogclearlove blog
set
代码语言:javascript复制set是es6新出来的一种数据结构,类似于数组,只是他的每一项都不会重复 例子c-a12
//c-a12
let s = new Set()
s.add(1)
s.add(2)
s.add(3)
s.add(1)
console.log(s.size) //3
s.delete(3)
console.log(s) // { 1, 2 }
s.clear()
console.log(s) //{}
利用set进行数组的去重
代码语言:javascript复制let r = [111, 2, 3, 4, 3, 2, 3, 4, 5]
let res = new Set(r)
console.log(res) // { 111, 2, 3, 4, 5 }
let ks = [...res]
console.log(ks) //[ 111, 2, 3, 4, 5 ]
let as = Array.from(res)
console.log(as) //[ 111, 2, 3, 4, 5 ]
遍历set
代码语言:javascript复制let s = new Set()
s.add(1)
s.add(2)
s.add(3)
s.add(1)
s.forEach(i => {
console.log(i)
})
// 1,2,3
区别 let const var
let
- 具备块级作用域
代码语言:javascript复制let的出现是为了完善js本身的语言的缺陷,我这么说可能不太恰当,js在let出现之前,只有全局作用域和局部作用域,java中也是一样,但是let 出现了之后出现了块级作用域,也就是被{}包含的作用域 比如: c-l
// c-l
if (1) {
let a = 1
}
console.log(a) //a is not defined
if(1){
var a = 1
}
console.log(a) //1
- for循环中 let也具备一定的块级作用域的能力 我们在一个for循环之后,声明的变量还可以在for循环结束进行访问,其实是不合理的,比如
for (var i = 0; i< 9; i ) { }
console.log(i) //9
代码语言:javascript复制使用let 进行改变写法就不会出现这个问题
for (let i = 0; i < 9; i ) { }
console.log(i) //i is not defined
- Let 不存在变量提升
console.log(a)
let a = 1 //Cannot access 'a' before initialization
- 存在暂时性死区
//块级作用域中和{}绑定的,所以外部的是没办法使用的
var a = 1
if (1) {
console.log(a) //Cannot access 'a' before initialization
a = 2
let a = 1
}
const
- 用来声明常量,不予许改变
const PI = 3
PI = 4
console.log(PI) //Assignment to constant variable
- 一样存在暂时性死区
//块级作用域中和{}绑定的,所以外部的是没办法使用的
var a = 1
if (1) {
console.log(a) //Cannot access 'a' before initialization
a = 2
const a = 1
}
- Const 必须赋初始值
const a
console.log(a) //Missing initializer in const declaration
var
- 存在变量提升
- 函数级的作用域
- 值是可以修改的
var因为用的太久了,所以这里不做过多的代码演示
解构赋值操作
- 数组的解构
// 数量必须是一一对应的,否则就是undefined
let r = ['jim', 29, '北京']
let [name, age, address] = r
console.log(name) //jim
console.log(age) //29
console.log(address) //北京
console.log(other) //undefined
- 对象的解构
let o = {
name : 'jim',
age : 20,
add : '河南'
}
let {name,age,add,other} = o
console.log(name) //jim
console.log(age) //20
console.log(add) //河南
console.log(other) //undefined
- 改变变量名字
//对象的解构是可以直接进行重命名的,但是数组不可以
let o = {
name: 'jim',
age: 20,
add: '河南'
}
let { name: aName, age: aAge, add: aAdd, other: aOther } = o
console.log(aName) //jim
console.log(aAge) //20
console.log(aAdd) //河南
console.log(aOther) //undefined
箭头函数
代码语言:javascript复制函数的语法糖,也就是一种函数的简单方式,他没有自己的this,所以他的this指向的是他的上下文,因为没有this,所以没办法使用appay、call等方法进行改变this的指向 例子c-j
//c-j
let n = {
name: 'kim'
}
let o = {
name: "jim",
fn: () => {
console.log(this) //window
},
fn1: function () {
console.log(this) //kim
}
}
o.fn.call(n)
o.fn1.call(n)
模板字符串
代码语言:javascript复制es6中给我们提供了模板字符串的相关操作,简化了我们的代码写法 例子c-m
//c-m
let name = "jim"
let age = 20
console.log(`我的名字是${name},我的年龄是${age}`) //我的名字是jim,我的年龄是20
- 还可以进行函数的执行
let sayH = (name)=>{
return `我的名字是${name}`
}
console.log(`${sayH(name)}`) //我的名字是jim
写到最后
其实关于js高级部分还有很多,但是考虑到篇幅和个人能力问题,暂时先写这么多吧,已经肝了两个夜晚了,每一个例子都是自己写的, 也都是经过验证的,所以你们看的时候大可放心的直接拿去使用,有什么问题可以直接下方联系我,关于js部分暂时就更新这些吧,后面我可能要去更新关于TS的知识点, 不太会去总结js相关的了,不过你们希望了解的一些知识点可以下方留言,我看到了,会在这篇文章里面直接更新的,感谢大家的阅读,拜了个白!