这篇文章总结一下js中高级中存在的一些易错和易混的概念型的问题,主要包含了一下一个模块
每一个知识点我都会尽可能的讲明白,写一些Demo给你们,就像前面写canvas的时候一样,尽量多写一些有说明性的代码,两个目的,第一个是总结一下,第二个是进行一个记录,也给学习js的过程中比较迷茫的一些提示,这篇文章是属于js中相对中高级的,所以初级的看起来会有一些困难,但是初级的可以直接百度或者看我之前的一些关于js的文章进行学习也是可以的!虽然上面的每一个点都是可以直接单独拿出来写一篇文章的,我也不是没这么计划,只是觉得这样会显的这个知识点好像很难一样,会劝退一部分人,所以就直接一篇文章直接搞定算了,篇幅会比较长,和前面的小程序和canvas一样,因为最近都是写一些总结性质的文章,可能会相对比较繁琐,读的时候可以直接收藏,后面慢慢看!我也会尽量的将每一个模块都分的比较清楚!
概念解释:构造函数也是一种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] }
animal.eatFood()
animal.aName
animal.aGender
Ani.age = 12 //age就是静态成员,直接通过构造函数进行添加,同时只能通过构造函数进行访问,实例对象是不可以访问的
console.log(Ani.age)
如果构造函数里面没有引用数据类型全部都是基本数据类型的话,是不会造成内存浪费的情况的,说简单一些,因为引用数据类型在被使用的时候,系统会重新添加一块内存空间出来,为了解决这个问题,一般引用数据类型或者说是公共方法建议是直接写到构造函数原型上进行共享内存地址。通过原型分配的函数是所有成员共享的,所谓的原型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
通过上面的例子,我们可以看到,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__()
*/
//方法查找规则演示 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('小鱼干')
// 不吃小鱼干
实例对象原型__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
//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
// }
这里我为什么加上双引号呢?因为构造函数本身是没有继承的,最起码在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__也指向了构造函数,有点绕,但是理解了 ,就比较容易了!关于构造函数我们就说这么多!
原型链是一个比较难以理解的东西,但是只要上面的构造函数搞明白了,这个原型链应该也问题不大,下面我们简单的说一下什么是原型链条,这个难为很多人的知识点,例子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
上面通过一段代码展示了什么是原型链条,他的作用是什么呢?原型链的一个主要作用就是进行属性方法的查找,当访问一个实例对象的属性时,是按照下面的顺序进行查找的
例子c-8
// 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
//c-9
//创建一个动物类
class Ani {}
//创建一个实例对象
let d = new Ani()
构造函数部分已经讲过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的时候被执行了
类和类 之间是可以进行继承,子类继承了父类之后,也就继承他的所有属性和方法,也就是说,父类的方法和属性都可以被子类给复用 例子 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的使用,在子类继承了父类之后,因为每一个类都有自己的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
// 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
// c-14
class F {
ordinaryFunction() {
console.log("我是F类")
}
}
class C extends F {
ordinaryFunction() {
console.log("我是C类")
}
}
let c = new C()
c.ordinaryFunction() // 我是C类
//类继承之后方法查找顺序:实例对象查找方法先在本身查找,找不到才会去父类查找
调用父类的普通函数 类继承之后方法查找顺序:实例对象查找方法先在本身查找,找不到才会去父类查找
// c-14
class F {
ordinaryFunction() {
console.log("我是F类")
}
}
class C extends F {
ordinaryFunction() {
super.ordinaryFunction()
}
}
let c = new C()
c.ordinaryFunction() // 我是F类
子类继承了父类的方法之后,很多时候我们都是需要进行有自定义的函数处理一些业务的,这个时候我们可以将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
//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指向为什么没有验证,两个原因,验证的过程需要截图,本身篇幅就已经很长了,图片会显示的更长, 第二是 这种验证你们自己去实际操作一下比较直观也比较容易记得住,所以我这里就不一一验证了,但是上面的代码都是自己验证过的,都是没有问题的,有任何问题可以直接私信我或者下方留言!
在说闭包之前首先要明白的一个点是作用域的概念,在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
面试的时候经常会问到的一个问题就会闭包会产生内存泄漏的问题,首先这个问题要搞明白的一个点是什么叫做内存泄漏,在js中内存泄漏指的是程序运行结束,未使用的或者已经被使用过的变量应该得以销毁却没有被销毁的现象就叫做内存泄漏,那么根据我前面关于js内存清除机制可以知道,js是由一个root节点进行查找该变量有没有被使用来判断他是不是需要销毁的,普通函数执行结束就不会被root节点找到,所以会被销毁,但是闭包因为是内部调用,所以js在查找的时候找到内部函数还在调用这个变量,那么这个时候他不会主动销毁这个变量,所以就导致了内存泄漏!
this的指向问题一只都是一个比较头疼的问题,因为涉及到的的情况比较多,其实我自己也不敢保证所有的情况我都是很确定的,很多时候我也是写一个demo自己尝试一下,看看this的指向到底是什么,所以下面的例子是我写的过程中觉得比较有意义的一些,希望可以帮助大家理解这一块的内容 一般的原则是 this指向的都是调用者
在没有进行说this指向问题之前,首先要搞明白的事情是,函数的多种定义方式,因为不同的定义方式意味着this的指向是不同的,下面罗列几种函数常见的定义方式
// 自定义函数 c-z
function f(){}
// 函数表达式 匿名函数
let f = function(){}
// new Function
let n = new Function()
// 所有的函数都是Funtion的实例对象
//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
//c-17
let that
function F(){
that = this
}
let f = new F()
console.log(f === that) // true
let _this
function F() {}
F.prototype.move = function () {
_this = this
}
let f = new F()
f.move()
console.log(f === _this) //true
call
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
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
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)
}
这个东西如果你会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 }
//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' }
这里不做具体的讲解,自己试试就好了,没有什么难以理解的,最好的方式是自己每一个属性都试试
//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
//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
// 直接赋值 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()
浅拷贝对于拷贝原对象来说其实就是一个地址,基本数据类型没有新地址,引用数据类型存在新的地址,那么其实我们拷贝了一个地址的话,就意味着拷贝的对象改变会导致原对象也进行变化 例子: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
//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
/**
*
* @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 所以这里的判断条件不可以写反,否则会导致拷贝不彻底
高阶函数其实我们经常使用,只是我们使用的过程中不知道他是一个高阶函数而已,高阶函数满足两个条件中的任意一个都可以,第一个是 他的形参数是一个函数,第二个是他的返回值是一个函数 例子 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
递归指的是函数本身自己调用自己的过程就叫做递归,这种写法的函数就叫做递归函数
递归一般都是举这个例子,我们也写一下,所谓的阶乘就是 1234567…进程乘积,给定一个实参,进行计算参数以内的乘积 例子 c-d
//c-d
let jc = function (n) {
if (n === 1) {
return 1
}
return n * jc(n - 1)
}
console.log(jc(10)) //3628800
//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 }
}
当实参大于形参数个数的时候,我们可以使用剩余参数的写法进行函数的定义 例子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
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
//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' }
将伪数组(可遍历对象)改为真数组 例子 c-a4
//c-a4
let r = {
'0': '1',
'1': '2',
'2': '3',
length : 3
}
let nr = Array.from(r)
console.log(nr)
查找符合条件的第一个数据,只会找到第一个 就不会再继续查找了 例子 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' }
查找数组中是不是每一项都符合 返回的是一个boolean 例子 c-a6
//c-a6
let eo = fo.every((item)=> item.name === 'jim')
console.log(eo) //false
查找数组中是不是存在一项 返回的是一个boolean. 例子c-a7
//c-a7
let eo = fo.some((item)=> item.name === 'jim')
console.log(eo) //true
查找数组中符合条件的第一个值的索引 例子c-a8
//c-a8
let eo = fo.findIndex((item)=> item.name === 'jim')
console.log(eo) //0
查看数组是否包含某一项 例子c-a9
//c-a9
let no = [1,23,4,4]
let r = no.includes(4)
console.log(r) //true
字符串是否以**开头或者结尾 例子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
将字符串复制几份 例子c-a11
//c-a11
let str = 'clearlove blog'
let c = str.repeat(4)
console.log(c) //clearlove blogclearlove blogclearlove blogclearlove blog
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) //{}
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 ]
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的出现是为了完善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 (var i = 0; i< 9; i++) { }
console.log(i) //9
使用let 进行改变写法就不会出现这个问题
for (let i = 0; i < 9; i++) { }
console.log(i) //i is not defined
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 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 a
console.log(a) //Missing initializer in const declaration
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
函数的语法糖,也就是一种函数的简单方式,他没有自己的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)
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相关的了,不过你们希望了解的一些知识点可以下方留言,我看到了,会在这篇文章里面直接更新的,感谢大家的阅读,拜了个白!