一、面向对象
1.1 类与对象初识
对象是由属性和方法组成的:
- 属性:事物的特征,在对象中用属性来表示。
- 方法:事物的行为,在对象中用方法来表示。
类:
- 在ES6中新增加了类的概念,可以使用class关键字声明一个类,之后以这个类来实例化对象。
类抽象了对象的公共部分,它泛指某一大类(class)
对象则是特指某一个,通过类实例化一个具体的对象。
面向对象的思维特点:
- 抽取对象共用的属性和行为,封装成一个类。
- 对类进行实例化,获取类的对象。
创建类
//格式:
class 类名 {}
类constructor构造函数
constructor()方法是类的构造函数(默认方法),用传递参数,返回实例对象.
通过new命令生成对象实例时,自动调用该方法,如果没有显示定义,类内部会自动给我创建一个constructor()
创建类,并实例化对象
//创建人物类
class People {
//类的共有属性放到constructor里面
constructor(uname) {
this.uname = uname
}
}
//利用类实例化对象
var cxk = new People('社会我坤哥')
//打印该对象的uname属性
console.log(cxk.uname)
既然公用的属性是放到了constructor里面,那么公用的方法放到哪里呢
class People {
constructor(uname) {
this.uname = uname
}
//共有的方法,对象可直接“.”调用
specialty () {
return '唱跳rap篮球'
}
}
var cxk = new People('社会我坤哥')
var iKun = new People('我是真挨坤')
//两个对象都可以调用特长方法。
console.log(cxk.specialty())
console.log(iKun.specialty())
二、ES6类继承-1
2.1 extends与super
与CSS类的继承一个道理,只不过在ES6中,类的继承需要使用extends或super关键字。
extends
代码示例:
class Father { money() { return '$100W' } } class Son extends Father { } let son = new Son() console.log(son.money())
详解:
- 先声明了一个Father类,改类里面有个公共方法为money。
- 在声明一个Son类,该类继承了Father类中的方法。
- 通过Son类实例化的对象,可以调用Son类继承的方法(Father类)
super
作用:
- 当需要将子类的属性传递给父类使用时,就需要用到super方法。
代码示例:
class Father { constructor(number1,number2) { this.num1 = number1 this.num2 = number2 } count() { return this.num1 + this.num2 } } class Son extends Father { constructor(number1,number2) { //调用了父类中的构造函数 super(number1,number2); //将7和9传入到父类的constructor里。 } } let son = new Son(7,9) console.log(son.count())
注:
-
子类在构造函数中使用super,必须放到this前面。
-
在继承中,遵循就近原则,当子类有该属性或方法时,不在从父类中找。
如果想要在子类中调用父类的方法,可以使用super,如:
super.父类方法()
三、ES5的继承
在ES6之前,Js中并没有引入类的概念,对象不是基于类创建的,而是一种称为构造函数的特殊函数来定义对象和他们的特征。
如:
function Star(uname) {
this.uname = uname
this.sing = function () {
return '唱歌'
}
}
let ldh = new Star('刘德华')
console.log(ldh.uname)
console.log(ldh.sing())
//控制台输出:
"刘德华"
"唱歌"
3.1 成员
实例成员与静态成员
-
实例成员就是构造函数内部通过this添加的成员,实例成员只能通过实例化的对象来访问。
function Star(name,age) { this.name = name this.age = age } //name和age属性,就是示例成员
-
静态成员就是在构造函数本身添加的成员,静态成员只能通过构造函数来访问。
function Star() { } Star.sex = '男' //sex属性为静态成员
3.2 原型对象prototype
构造函数存在的问题
以上节代码(Star构造函数)为例,由于sing方法属于复杂数据类型,所以在内存中需要另外开辟空间进行存储(栈),当通过构造函数实例化了N个对象时,就需要开辟N个空间用来存放sing的值。
sing为每个对象公用的方法,每个对象调用都是一样的,所以当每个对象都需要另存一份时,这就造成了资源浪费。
解决办法:利用prototype
- 将不变的方法直接定义在prototype对象上,这样所有对象的实例就可以共享这些方法了。
function Star(uname) {
this.uname = uname
}
Star.prototype.sing = function () {
return '唱歌'
}
let ldh = new Star('刘德华')
let gtl = new Star('古天乐')
console.log(ldh.sing())
console.log(gtl.sing())
//直接对比,对比的是内存地址,判断是否指向的是一个地址。
console.log(ldh.sing == gtl.sing)
//控制台输出:true 指向的是同一个。
我们创建的每个函数都有一个 prototype (原型)属性,这个属性是一个指针,指向一个对象, 而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法,称为原型对象
原型:
- 构造函数的prototype属性。
原型对象:
- 构造函数通过原型,来赋与属性与方法的对象,这个对象内的属性和方法,可以共享。
原型对象的作用:
- 共享属性和方法
3.3 对象原型_proto_
对象为什么可以使用构造函数prototype原型对象的属性和方法呢?
因为对象中有__proto__属性的存在。
对象会自动添加一个__proto__属性,指向我们构造函数的原型对象,从而调用原型对象中的属性和方法。
//ldh为对象,Star为构造函数 console.log(ldh.__proto__ == Star.prototype) //控制台打印: "true"
可以发现,Star.prototype指向的原型对象,与ldh.__proto__指向的原型对象,是一致的。
''' __proto__对象原型存在意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性,因此实际开发过程中,不可以使用这个属性,它只是内部指向原型对象prototype '''
3.4 原型constructor
作用:
- 在原型中有这个constructor属性,通过它可以知道,对象是通过那一个构造函数创建出来的。
在使用时,更多的是手动利用constructor这个属性,指回原来的构造函数。
什么时候需要手动指回呢?
如果修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动利用constructor这个属性,指回原来的构造函数。
如:
function Star() { } Star.prototype = { sing:function () { return '唱歌' }, movie:function () { return '拍戏' } }
let test = new Star() console.log(test.constructor) //浏览器打印: "ƒ Object() { [native code] }"
可以发现,test对象指向的是Object,而不是Star。 在这种情况下,就需要手动指向。
function Star() { } Star.prototype = { //指向原来的构造函数 constructor: Star, sing:function () { return '唱歌' }, movie:function () { return '拍戏' } } let test = new Star() console.log(test.constructor) //浏览器打印: "ƒ Star() {}"
3.5 原型链
prototype与_proto_、constructor三者的关系如下图:
Object原型对象prototype理解为全局变量,当构造函数没有利用原型共享属性,而对象示例也没有这个属性时,回去找Object,如果还没有则返回null。
代码示例:
一、当构造函数的原型对象prototype有该成员时
function Test() { } Test.prototype.uname = 'liuyu' let t = new Test() console.log(t.uname) //控制台输出:liuyu
二、当只有Object原型对象有该成员时
function Test() { } // Test.prototype.uname = 'liuyu' Object.prototype.uname = 'liuyu' let t = new Test() console.log(t.uname) //控制台输出:liuyu
三、当构造函数和Object原型对象都没有时
function Test() { } let t = new Test() console.log(t.uname) //控制台输出:undefined
对象成员查找规则,是按照原型链的顺序进行查找的
3.6 原型对象this指向
this指向:
- 在构造函数中,this指向的是实例对象。
- 在原型对象函数里面的this,指向的是实例对象。
- 二者并未差别。
3.7 原型对象的应用
利用原型对象,扩展内置对象方法
一、查看Array数组的原型对象
console.log(Array.prototype)
可以看到有push、reverse、concat、length等等常用的内置方法,但是没有给所有元素求和的。
二、给Array类添加成员(方法)
Array.prototype.sum = function () { let sum = 0 for (let i=0; i<this.length; i++) { sum += this[i] } return sum } let arr = new Array() arr = [1,2,3] console.log(arr.sum()) //控制台输出: 6
因为arr是Array类实例化出来的,所以我们只需要给Array的原型对象中,添加这个方法即可,按照成员查找顺序,可以查找到。因此所有的arr数组都可以调用sum这个方法。 注意: - 不能以大括号(对象)的形式,否则会覆盖掉原有的方法。
3.8 利用构造函数继承
ES6之前并没有给我们提供extends继承,我们可以通过构造函数+原型对象模拟实现继承,被称为组合继承。
本章节为利用构造函数继承
前言:
方法名: call()
作用:
- 可以调用函数。
- 可以改变这个函数的this指向。
代码示例:
function test(num1,num2) { console.log('调用了') console.log(this) console.log(num1+num2) } obj = {name:'liuyu'} test.call(obj,3,4) //控制台输出: // 调用了 // Object {name:'liuyu'} // 7
发现:
- 直接调用call()可以直接运行函数。
- 并且this指向的是obj这个对象。
- 后面跟的3和4两个参数,也被test函数所接收,并输出相加后的结果。
正片
应用:借用(继承)父构造函数的属性及方法-1
function Father() {
this.hobby = '吃喝睡玩'
this.smile = function () {
console.log('哈哈哈哈哈哈')
}
this.cry = function () {
console.log('呜呜呜呜呜呜呜呜')
}
}
function Son() {
Father.call(this)
this.cont = '我是子构造函数'
}
let son = new Son()
console.log(son.cont)
console.log(son.hobby)
son.smile()
son.cry()
// 控制台输出:
"我是子构造函数"
"吃喝睡玩"
"哈哈哈哈哈哈"
"呜呜呜呜呜呜呜呜"
流程:
- 通过构造函数实例化对象son,该对象有公共属性cont。
- 在Son构造函数内部,让父构造函数Father调用了call方法,将Son里面的this传了过去,由Father接收,最终Father内部的this就编程了son对象。
- 所以son对象可以调用自身的cont属性,可以调用Father构造函数中的hobby属性、smile方法、cry方法。
应用:借用(继承)父构造函数的属性及方法-2
function Father(uname,age) {
this.uname = uname
this.age = age
}
function Son(uname,age,sex) {
Father.call(this,uname,age)
this.sex = sex
}
let son = new Son('liuyu',22,'男')
console.log(son)
// 控制台输出:
// Son {uname: 'liuyu', age: 22, sex: '男'}
流程:
- 通过构造函数Son实例化对象son,参数为’liuyu’,22,‘男’
- 在Son构造函数内,给son对象添加了属性sex,值为传入的值’男’
- 由于在Son构造函数内使用了call方法,所以Father内部的this,就成了son
- 因此this.uname == son.uname,而在Son中又将uname传到了Father。
- 所以最终son对象拥有三个属性。
3.8.1 优缺点
优点:
- 可以继承父构造函数的属性及方法
缺点:
- 继承不了原型链上的属性及方法
验证缺点
function Father() { } Father.prototype.eat = function () { console.log('干饭人!干饭魂!') } Father.prototype.other = '广告位招租' function Son() { Father.call(this) } let father = new Father() let son = new Son() father.eat() console.log(father.other) console.log(son.other) son.eat() //控制台输出: '干饭人!干饭魂!' '广告位招租' 'undefined' 'TypeError:son.eat is not a function'
可以看到,Father这个构造函数实例化出来的father对象,可以调用原型链上的方法,但是通过构造函数继承的son对象则不可以继承原型链上的属性和方法。
3.9 利用原型对象继承
引子:
- 既然父构造函数的原型对象,子构造函数实例化的对象无法访问,那直接Son.prototype == Father.prototype不就好了吗?
验证:直接让子构造函数的原型对象 == 父构造函数的原型对象行不行?
function School() { this.schoolName = '中国地质大学(北京)' this.addr = '海淀区学院路29号' } School.prototype.college = function () { return '信息工程学院' } Student.prototype = School.prototype function Student() { School.call(this) } let liuyu = new Student() //schoolName和addr属性,是利用上一节中的构造函数继承。 console.log(liuyu.schoolName) console.log(liuyu.addr) //通过原型对象,集成到college方法 console.log(liuyu.college()) //控制台输出: '中国地质大学(北京)' '海淀区学院路29号' '信息工程学院' //可以看到,无报错,正常调用原型对象中的college方法
但是
由于子构造函数与父构造函数做了绑定,当子进行更改时,父会跟着更改。
验证代码如下:
function School() { } School.prototype.college = function () { return '信息工程学院' } function Student() { } Student.prototype = School.prototype Student.prototype.daily = function () { return '上课吃饭睡觉' } let liuyu = new Student() let school = new School() console.log(liuyu.college()) console.log(liuyu.daily()) console.log(school.college()) console.log(school.daily()) //控制台输出: "信息工程学院" "上课吃饭睡觉" "信息工程学院" "上课吃饭睡觉"
结果可以看到,school对象可以调用daily方法,日常是“上课吃饭睡觉”,这显然并不合理。
正片
正确思路:
Student.prototype = new School() //因为new School()是个对象,当我们重新赋的值为对象时,需要重新指定constructor,指回原来的构造函数 Student.prototype.constructor = Student
解析:
- Student.prototype = new School()
- School构造函数实例化出来的对象,可以访问School原型对象,而这个对象的内存地址,与原型对象内存地址不一样,不存在相互影响。
- 然后我们给Student的原型对象中添加方法时,由于没有直接与School原型对象做绑定,所以school对象就不能调用。
代码示例:
function School() {
}
School.prototype.college = function () {
return '信息工程学院'
}
function Student() {
}
Student.prototype = new School()
//因为new School()是个对象,当我们重新赋的值为对象时,需要重新指定constructor。
Student.prototype.constructor = Student
Student.prototype.daily = function () {
return '上课吃饭睡觉'
}
let liuyu = new Student()
let school = new School()
console.log(liuyu.college())
console.log(liuyu.daily())
console.log(school.college())
console.log(school.daily())
//控制台输出:
"信息工程学院"
"上课吃饭睡觉"
"信息工程学院"
"school.daily is not a function"
四、 ES5中新增的方法
4.1 数组方法
迭代(遍历)方法:forEach()、map()、filter()、some()、every()
语法 | 作用 |
---|---|
数组.forEach(function(value,index,array){}) | 遍历数组 |
数组.filter(function(value,index,array){}) | 遍历筛选数组 |
数组.some(function(value,index,array){}) | 遍历数组并进行判断 |
forEach
作用:
- 遍历数组
格式:
数组.forEach(function (value,index,array) {})
value:每次遍历数组拿到的值。
index:拿到value的索引。
array:数组本身
代码示例:
let arr = [1,2,3,4,5] arr.forEach(function (value,index,array) { console.log('值为'+value,'索引为'+index,'数字本身为'+array) }) //控制台输出: // 值为1 索引为0 数字本身为1,2,3,4,5 // 值为2 索引为1 数字本身为1,2,3,4,5 // 值为3 索引为2 数字本身为1,2,3,4,5 // 值为4 索引为3 数字本身为1,2,3,4,5 // 值为5 索引为4 数字本身为1,2,3,4,5
应用:计算出数组内每个元素的和,除了数组内索引为2的。
let arr = [1,2,3,4,5] let sum = 0 arr.forEach(function (value,index,array) { if (index !== 2) sum += value }) console.log(sum) //控制台输出: 12
filter
作用:
- 主要用于筛选。
- 另外需要注意,该方法会直接返回一个新数组
格式:
array.filter(function(value,index,array))
value:每次遍历数组拿到的值。
index:拿到value的索引。
array:数组本身
代码示例:
//筛选出数组内值大于20的 let arr = [1,19,37,48,62,9] let newArr = arr.filter(function (value) { return value > 20 }) console.log(newArr) //控制台输出:[37, 48, 62]
- filter会返回符合条件的值,并组成列表,由netArr接收,不会对原列表有任何影响。
some
作用:
- 与filter类似,用于筛选,不过some返回的是布尔值
- 并且找到值之后,就不在迭代了,也正以为如此,当值唯一时,some的效率最高。
格式:
arr.some(function(value,index,array){})
代码示例:
一
//查询列表中有没有等于9的值 let arr = [1,7,9,10,12] let newArr = arr.some(function (value) { if (value == '9') { return true } }) console.log(newArr) //控制台输出: true
二
//查询列表中有没有等于0的值 let arr = [1,0,4,6] let flag = arr.some(function (value) { return value == 0 }) console.log(flag) //控制台输出:true
注:
- some内部的return true表示找到了就不在迭代了,这一点与forEach和filter不同,这两者都会一直迭代下去。
疑问?为什么在示例一种,要手动return true呢?
- 因为some的机制是,只要不返回true,那么就一直迭代,所以当我们查询到有该值时,要及时中断。
- 而在示例二中,我们直接返回的是value == 0,这本身就是个布尔值,所以可以直接return,返回true那就是有该值,反之则没有。
4.2 字符串方法
trim() 去除字符串两端的空格
4.3 对象方法
语法 | 作用 |
---|---|
Object.keys(obj) | 返回对象自身的所有属性,数据格式为数组 |
Object.defineProperty(pbj,prop,descriptor) | 定义或修改对象中的属性 |
内置方法:keys()
作用:
- 返回对象自身的所有属性key,数据格式为数组
格式:
Object.keys(obj)
代码示例:
let liuyu = {'name':'liuyu','school':'中国地质大学(北京)','add':'上海'} let liuyuKeys = Object.keys(liuyu) console.log(liuyuKeys) //控制台输出: ['name', 'school', 'add']
内置方法:defineProperty()
作用:定义对象中新属性或者修改原有的属性
格式:
Object.defineProperty(pbj,prop,descriptor)
obj:必填参数,目标对象
prop:必填参数,需要定义或者修改的属性名字
descriptor:必填参数,目标属性所拥有的特性,以对象形式书写,格式如下:
- value:设置属性的值,默认为undefined
- writable:值是否可以重写,true或false,默认为false
- enumberable:目标属性是否可以被枚举,true或false,默认为false
- configurable:目标属性是否可以被删除,或者是否可以再次修改特性,true或false,默认为false
注(以writable为例):
-
对象内原本就有的属性,均可通过defineProperty以及.属性重新赋值的方式进行修改。
-
对象内原有的数据,如果经过defineProperty设置为值不可重写时,传统的.属性方法无法修改值,只能通过defineProperty
-
使用defineProperty添加的属性,默认不可以被修改(defineProperty和直接.属性赋值,都不可以)
-
使用defineProperty添加属性,并且将特性改为不可重写,那么该属性将一直不能重写,再次修改特性会报错:
Cannot redefine property: publish at Function.defineProperty '''无法重新定义属性:发布'''
-
遍历(枚举)enumberable、是否可删除configurable,这二者的原理与writable一致。
代码示例:
一、
let obj1 = {'id':'1','bookName':'《三国演义》','author':'罗贯中'} Object.defineProperty(obj1,'id',{writable:false}) obj1.id = 3 //无法修改。 //可以修改 Object.defineProperty(obj1,'id',{value:2}) console.log(obj1) //控制台输出: {id: 2, bookName: '《三国演义》', author: '罗贯中'}
- id这个属性为对象自带的,非defineProperty定义的,所以不管writable的值是什么,都不影响defineProperty再次修改。
二、
let obj1 = {'id':'1','bookName':'《三国演义》','author':'罗贯中'} Object.defineProperty(obj1,'publish',{value:'人民出版社'}) // obj1.publish = '上海图书出版社' 无报错,但不能修改。 Object.defineProperty(obj1,'publish',{value:'上海图书出版社'}) console.log(obj1) //控制台输出:TypeError: Cannot redefine property: publish
- 可以看到,我们使用defineProperty新定义一个属性名publish,后续不管是那种方法,都不能进行修改,因为新定义的默认writable为false,不可修改。
五、ES6的继承-2
ES6之前,通过构造函数+原型对象实现面向对象编程。
ES6,通过类实现面向对象编程。
类的本质还是function函数。
function School() { } console.log(typeof School) //控制台输出: "function"
既然本质就是函数,也可以认为,类就是构造函数的另外一种写法。
构造函数的特点:
- 有原型对象prototype
- 原型对象里面有constructor,指向构造函数本身
- 可以通过原型对象添加方法
- 创建的实例对象有_proto_,指向构造函数的原型对象。
类同样具有构造函数的特点,可以通过通过prototype原型对象添加方法,实例化的对象也有constructor属性指向构造函数。
所以ES6的类,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰,更想面向对象编程的语法而已。
ES6的类,其实就是语法糖。
-
语法糖就是一种便捷写法,写法更加清晰方便的,这种方法就叫做语法糖。
-
如:
i = i + 1; i++ //语法糖
六、函数
6.1 函数的定义方式
命名函数,函数声明方式function关键字
匿名函数,函数表达式
new Function()
代码示例:
1.命名函数
function func() {};
2.函数表达式
let func = function() {}
3.利用new Function
let func = new Function('形参1','形参2',.....,'函数体')
例如:
let func = new Function('num1','num2','return num1+num2') console.log(func(1,2)) //浏览器控制台输出: 3
Function里面的参数,都必须是字符串。
该方式了解即可,效率低。
6.2 函数的调用方式
1.普通函数
function func() { console.log() } func() //或者 func.call()
2.对象的方法
let liuyu = { 'name':'liuyu', 'school':function() { console.log('中国地质大学(北京)') } } liuyu.school()
3.构造函数
function Student() {} new Student()
4.绑定事件函数
btn.onclick = function() {} //点击触发事件,调用函数。
5.定时器函数
setInterval(function () { console.log('打印了') },1000) //该定时器每间隔1000毫秒调用一次。
6.立即执行函数
(function(){})() //页面加载完毕立即执行。
附:立即执行函数两种格式:
(function(形参) {})(实参) (function(形参) {}(实参))
6.3 不同调用方式的this指向
-
普通函数,this指向window
function func() { console.log(this) } func() //Window {window: Window, self: Window, document: docu.......
-
对象的方法,this指向函数的调用者(对象)
let liuyu = { 'test':function() { console.log(this) } } liuyu.test() //{test: ƒ}
-
构造函数,this指向调用者
function Test() { this.test = function () { console.log(this) } } let obj1 = new Test() obj1.test() //Test {test: ƒ}
-
绑定事件函数,this指向触发事件的按钮
// HTML标签 <button>按钮</button> let btn = document.querySelector('button') btn.onclick = function () { console.log(this) } //<button>按钮</button>
-
定时器函数,因为格式是以window开头的,只是因为可以省略而已,所以this指向肯定还是window
setInterval(function () { console.log(this) },1000) //Window {window: Window, self: Window, document: docu.......
-
立即执行函数,this指向window
(function(){ console.log(this) })() //Window {window: Window, self: Window, document: docu.......
6.4 改变函数内部this指向
JavaScript专门提供了一些函数方法,用来处理函数内的this的指向问题,常用的有bind()、call()、apply()三种。
语法 | 作用 |
---|---|
函数.call() | 调用函数,改变函数内的this指向,多用于继承 |
函数.apply() | 调用函数,改变函数内的this指向,多用于数组操作 |
函数.bind() | 不会调用函数,改变函数内的this指向 |
6.4.1 call
call()方法
作用:
- 可以调用函数。
- 可以改变函数内的this指向,可实现继承。
格式:
func.call(函数运行时this指向,参数1,参数2,...)
代码示例:
let liuyu = { 'name':'liuyu', 'age':22 } function school(schoolName,college) { this.schoolName = schoolName this.college = college } school.call(liuyu,'中国地质大学(北京)','信息工程学院') console.log(liuyu) //{name: 'liuyu', age: 22, schoolName: '中国地质大学(北京)', college: '信息工程学院'}
执行思路:
- 定义了一个liuyu对象以及一个school函数。
- 函数.call()会直接调用函数,并且call()内的第一参数,会被函数内的this所接收,call()内的后面参数,会被school函数的形参接收。
- 由于school函数内的this,已经指向到了liuyu对象,所以函数体内部代码所定义的属性,会存在在对象中。
- 所以最后打印对象时,可以看到多出来了函数内的属性。
6.4.2 apply
apply()方法
作用:
- 与call所实现的效果是一样的
- 区别在于apply方法传的值,必须包含在数组里面
格式:
func.apply(函数运行时this指向,数组)
代码示例:
let liuyu = { 'name':'liuyu', 'age':22 } function school(schoolName,college) { this.schoolName = schoolName this.college = college } school.apply(liuyu,['中国地质大学(北京)','信息工程学院']) console.log(liuyu) //{name: 'liuyu', age: 22, schoolName: '中国地质大学(北京)', college: '信息工程学院'}
- apply的参数必须是数组。
- apply也会自动调用函数,在调用时,数组中的每个元素会作为参数,用于school函数的形参接收。
apply函数的主要应用:
-
比如:利用apply实现求数组中最大值。
//注:Math.max原本的用法:Math.max(1, 3, 2),求一组数中最大值 let arr = [1,24,5,18,6,9] let max = Math.max.apply(Math,arr) console.log(max) // 24
-
apply方法会调用max方法,同时会将apply参数的数组内部所有元素,作为参数,并用于max方法的形参接收,即:
Math.max.apply(Math,[1,24,5,18,6,9]) = Math.max(1,24,5,18,6,9)
-
由于我们不需要修改this指向,所以重新指回Math对象。
6.4.3 bind
bind()方法
作用:
- 与call和apply不同,bind不会调用函数,但是可以改变函数内部this指向。
- bind方法,会返回原来改变this指向之后的函数。
格式:
func.bind(函数运行时this指向,参数1,参数2,...)
代码示例:
let test = {'name':'liuyu'} function fn() { console.log(this) } let f = fn.bind(test) f() //{name: 'liuyu'}
- bind方法将fn函数内的this指向,指向了test对象,并且将fn函数内改完this指向的代码返回。
- f接收之后,直接加括号调用函数。
- 最后打印的this,为test对象。
应用:
-
如果有函数,我们不需要立即调用,但是又想改变这个函数内部的this指向,此时用bind方法即可。
-
案例:一个按钮,点击之后禁用3秒,3秒之后开启这个按钮。
初始代码:
$('button').on('click',function () { this.disabled = true setTimeout(function () { this.disabled = false },3000) })
问题?
-
定时器函数内部,this指向为window。
-
window.disable会报错。
解决方法:
- 定时器函数外面的this.disable = true,这里this指向的是触发事件的对象,为btn按钮。
- 那么我们想半天,让定时器内部的this,等于外面的这个this就好了。
最终版:
$('button').on('click',function () { this.disabled = true setTimeout(function () { this.disabled = false }.bind(this),3000) })
利用bind,将定时器函数外部的this,也就是btn对象,传给定时器内部。
-
6.4.4 总结
相同点
- 都可以改变函数内部的this指向。
区别点
- call和apply会调用函数,并且改变函数内部this指向。
- call和apply传递的参数不一样,apply必须数组形式。
- bind不会调用函数,但可以改变函数内部this指向。
主要应用场景
- call经常做继承。
- apply经常跟数组有关系。
- bind不调用函数,但是还想改变this指向。
6.5 高阶函数
什么是高阶函数:
- 高阶函数是对其他函数进行操作的函数,它接收函数作为参数或者将函数作为返回值输出,这样的函数就是高阶函数。
function func(callback) {
callback && callback()
}
func(function () {
console.log('测试')
})
代码详解:
- 定义了函数func,随后调用,传入参数,参数为匿名函数。
- 运行func函数时,callback形参接收匿名函数,随后进行判断。
- &&表示前面为真,才会执行后面,当调用函数时插入参数了,那么就加()调用该参数(匿名函数)。
- 由于func方法,接收函数作为参数,所以又叫做高阶函数。
6.6 闭包
回顾:变量的作用域
- 函数内部可以使用全局变量。
- 函数外部不可以使用局部变量。
- 当函数执行完毕,本作用域内的局部变量会销毁。
什么是闭包:
- 闭包指有权访问另一个函数作用域中变量的函数。
- 简单说就是,一个作用域可以访问另外一个函数内部的局部变量,这就是闭包。
闭包的作用:
- 延伸作用范围
代码示例:
function init() {
// name 是一个被 init 创建的局部变量
var name = "Mozilla";
// displayName() 是内部函数,一个闭包
function displayName() {
// 使用了父函数中声明的变量
alert(name);
}
displayName();
}
init();
利用闭包可以实现,在函数外面的作用域访问函数内的作用域。
function func() {
let name = 'liuyu'
return function () {
console.log(name)
}
}
let f = func()
f()
/*
等同于:
let f = function () {
console.log(name)
}
因为这个name属性为函数内部作用域,外面通过闭包的形式,在外部作用域也能访问到name属性。
*/
- 上述代码直接return了函数,所以也符合高阶函数的定义,所以闭包也是高阶函数的形式之一。
6.6.1 小案例
<ul style="list-style: none;">
<li>宫保鸡丁</li>
<li>鱼香肉丝</li>
<li>红烧排骨</li>
<li>紫苏牛蛙</li>
</ul>
需求:
- 点击Li标签返回当前索引号
初始代码:
let lis = document.querySelectorAll('li') for (let i=0;i<lis.length;i++) { lis[i].addEventListener('click',function () { console.log('输出当前li的索引号') }) }
- 先获取节点,随后依次给对应标签绑定事件,事件触发时输出“输出当前li的索引号”
- 所以我们下一步只需要解决,如何拿到索引号并输出。
回顾:立即执行函数语法
(function(形参) {})(实参)
(function(形参) {}(实参))
立即执行函数,可以传入参数,那么我们可以循环创建对应数量的立即执行函数,然后将“i”传给立即执行函数,随后再事件函数内部使用。
正式版
let lis = document.querySelectorAll('li') for (let i=0;i<lis.length;i++) { //创建的四个立即执行函数,i分别代表了0,1,2,3 (function (i) { //这个i是形参,用来接收参数 lis[i].addEventListener('click',function () { console.log(i) }) })(i) //这个i是实参,将0,1,2,3传给立即执行函数 }
最终实现,点击li标签,控制台打印改标签的索引号。
立即执行函数也是个小闭包,因为立即执行函数里面的任何一个函数(如事件函数),都可以使用它的i这个变量。
6.7 递归函数
什么是递归:
- 如果一个函数在内部可以调用其本身,那么这个函数就是递归函数。
- 简单来说就是,函数内部自己调用自己,那么这个函数就是递归。
- 递归函数的作用,和循环效果一样。
- 由于递归很容易发生**“栈溢出”错误(stack overflow),所以必须要加退出条件return**。
示例代码:
let num =1
function func() {
console.log('今天是想你的第'+num+'遍')
if (num == 9) {
return //递归必须添加退出条件
}
num++
//内部调用自己
func()
}
func()
递归的小案例:
需求:
- 求1~N的阶乘,1*2*3*4*…n
代码:
function fn(num) { if (num == 1){ return 1 } return num * fn(num - 1) } console.log(fn(3)) // 6
思路:
- 当函数第一次执行时,return 3 * fn(3-1)
- 此时需要计算fn(3-1)的值,fn(3-1) 也就是fn(2)
- 当函数第二次执行时,因为fn(2)这里的参数为2,也就是num等于2,所以return的是 2 * fn(2-1)
- fn(2-1) 等于 fn(1)
- 当函数第三次执行时,由于条件判断的存在,直接返回1
得到真实数字时候,在往上进行“换算”
fn(2-1) =fn(1) = 1
fn(3-1) = fn(2) = 2 * fn(2-1) = 2 * 1
最终结果 = 3 * fn(3-1) = 3 * fn(2) = 3 * 2 *1 = 6
6.8 深浅拷贝
浅拷贝:
- 浅拷贝只是拷贝一层,更深层次对象级别的只拷贝引用。
- Object.assign(target,…sources) ES6新增方法可以浅拷贝。
深拷贝:
- 深拷贝会拷贝多层,每一级别的数据都会拷贝。
浅拷贝
let obj = {'name':'liuyu','age':22,'addr':{'city':'上海','area':'徐汇区'}} let objBack = {} for (k in obj) { objBack[k] = obj[k] } obj.addr.area = '闵行区' console.log(objBack) //控制台输出:{'name':'liuyu','age':22,'addr':{'city':'上海','area':'闵行区'}}
- 因为浅拷贝在拷贝一些复杂数据类型是,拷贝过去的不是数据本身,而是内存地址。
- 所以当源数据修改之后,拷贝过去的会受到影响。
ES6提供的浅拷贝方法
格式:
Object.assign(拷贝给谁,源数据)
let obj = {'name':'liuyu','age':22,'addr':{'city':'上海','area':'徐汇区'}} let objBack = {} Object.assign(objBack,obj) console.log(objBack)
深拷贝
目前没有专门的方式用来做多层次的深拷贝,但可以通过很多方法来实现,如:
let obj = {'name':'liuyu','age':22,'addr':{'city':'上海','area':'徐汇区'}} let objBack = JSON.parse(JSON.stringify(obj)) obj.addr.area = '闵行区' console.log(objBack.addr.area) //控制台输出:徐汇区
可以发现,原对象obj的地区修改之后,我们拷贝过去的objBack并不会收到影响。
七、严格模式
JavaScript除了提供正常模式外,还提供了严格模式,ES5的严格模式是采用具有限制性JavaScript变体的一种方式,即在严格的条件下,运行JS代码。
严格模式在IE10以上版本的浏览器中才会被支持(目前IE已经歇菜)
严格模式对正常的JavaScript语义做了一些更改:
- 消除了JavaScript语法的一些不合理、不严谨之外,减少了一些“怪异”行为。
- 消除代码运行的一些不安全之处,保证代码运行的安全。
- 提高编译器效率,增加运行速度
- 禁用了在ECMAScript的未来版本中,可能会定义的一些语法,为未来版本的JavaScript做好铺垫,比如:class,extends,import等,不能做变量名。
7.1 如何开启严格模式
如何开启严格模式
严格模式可以应用到整个脚本或者个别函数中,因此在使用时,我们可以将严格模式分为为脚本开启严格模式和为函数开启严格模式两种情况。
一、为脚本开启严格模式
<script>
'use strict';
test = '123'
console.log(test)
</script>
<!--
控制台报错:Uncaught ReferenceError: test is not defined
-->
可以看到,开启了严格模式之后,变量不生命直接赋值会报错。
二、为函数开启严格模式
<script>
function fn() {
'use strict';
//下面的代码按照严格模式执行
}
function func() {
//下面的代码按照普通模式执行
}
</script>
7.2 严格模式的不同之处
- 一、变量名必须先声明再使用。
- 二、不能随意删除已经声明好的变量。
- 三、严格模式下,全局作用域中函数的this,为undefined。
- 四、严格模式下,构造函数不加new调用,this会报错。
八、正则表达式–暂时烂尾后续更新
正则表达式(Regular Expression),是用于匹配字符串中字符组合的模式,在JavaScript中,正则表达式也是对象。
正则表通常被用来检索、替换那些符合某个模式(规则)的文本,例如验证表单,用户名表单只能输入中英文字母、数字或者下划线等。
此外,正则表达式还常用于过滤掉页面内容中的一些敏感词(替换),或从字符串中获取我们想要的特定部分(提取)等。
8.1 创建正则表达式
第一种方式: 通过调用RegExp对象的构造函数创建
格式:
let 变量名 = new RegExp(/表达式/)
示例:
let rg = new RegExp(/123/)
第二种方式:利用字面量创建正则表达式
格式:
let 变量名 = /表达式/
示例:
let reg = /123/
正则表达式不需要加引号,直接在//里面书写即可。
测试正则表达式test
方法名:
- test()
作用:
- 用于检测字符串是否符合该表达式的规则,返回值为布尔。
格式:
regexObj.test(str)
示例:
let msg = '123' let regobj = /123/ console.log(regobj.test(msg)) //控制台输出: true
8.2 正则表达式的组成
一个正则表达式可以由简单的字符构成,比如/abc/,也可以是简单和特殊字符的组合,比如/ab^c/。
其中特殊字符也被称为元字符,在正则表达式中是具有特殊意义的专用符号,如^、$、+等。
边界符 | 说明 |
---|---|
^ | 以…开头 |
$ | 以…结尾 |
边界符代码示例
let reg1 = /123/ //包含123 console.log(reg1.test('123456')) //true let reg2 = /^123/ //以123开头 console.log(reg2.test('1234')) // true let reg3 = /^123$/ //以123开头,并且以123结尾。 console.log(reg3.test('1234')) // false
字符类
九、ES6
什么是ES6:
-
ES的全程是ECMAScript,它是由ECMA国际标准化组织,指定的一项脚本语言的标准化规范。
年份 版本 2015年6月 ES2015 2016年6月 ES2016 2017年6月 ES2017 2018年6月 ES2018 … … ES6实际上是一个泛指,泛指ES2015及后续的版本。
因为有太多东西需要变更了,不能一下子放到一个版本,所以拆分成了好几年,慢慢进行变更。
为什么使用ES6:
每一次标准的诞生都意味着语言的完善,功能的加强,JavaScript语言本身也有一些令人不满意的地方。
- 变量提升特性增加了程序运行时的不可预测性。(详情见预解析章节)
- 语法过于松散,实现相同的功能,不同的人可能会写出不同的代码,可读性差。
9.1 let
ES6中新增的用于变量声明的关键字
let声明的变量只在所处于的块级有效。
一个大括号内部就是一个块级作用域,如:
if (true) { let a = 10; } console.log(a) // error: a is not defined
好处:防止循环变量变成全局变量
特点:
- 没有变量提升。
- 暂时性死区特性。
暂时性死区特性:
let声明的变量会与当前块级做绑定,如果在声明之前就使用,会报错,且不会使用全局的变量。
let num = 10 if (true) { console.log(num) let num = 30 } //报错:Cannot access 'num' before initialization 初始化前无法访问“num”
var与let的对比
利用var声明的
if (true) { var a = 10 if (true) { var b = 20 } console.log(b) } console.log(a) //控制台输出: 20 10
利用let声明的
if (true) { let a = 10 if (true) { let b = 20 } console.log(b) //报错,未定义(超过了声明b的块级作用域) } console.log(a) //报错,未定义(超过了声明a的块级作用域)
经典面试题:
var arr = [] for (var i=0;i<2;i++) { arr[i] = function () { console.log(i) } } arr[0]() arr[1]() //控制台输出: 2 2
var声明的变量会提升到全局变量,所以console.log(i),引用的是全局变量i,而i经过for循环已经+到了2,所以两次打印都是2.
let arr = [] let i = 30 for (let i=0;i<2;i++) { arr[i] = function () { console.log(i) } } arr[0]() arr[1]() //控制台输出: 0 1
循环中let声明的变量会产生两个块级作用域,let i=0 let i=1 ,这两个作用域是互不影响的。
在函数console.log(i)执行时,由于函数这个块级没有变量i,所以会回到上一级作用域中进行查找,而上一级作用域呢,就是循环产生的块级作用域。
所以console.log(i)执行时,输出的是上级第一个块级作用域i的值,也就是let i=0,所以最后输出0,第二个输出1。
9.2 const
作用:
- 声明常量,常量就是值(内存地址)不能变化的量。
特点:
- 使用const关键字声明的常量具有块级作用域
- 声明的时候必须赋初始值
- 常量赋值后,值不能修改,但是当常量为数组时,是可以修改元素值的,因为内存地址并没有发生改变。
const str1 = '我是常量'
9.3 解构赋值
ES6中允许从数组中提取值,按照对应位置,对变量赋值,对象也可以实现解构。
数组解构允许我们按照一一对应的关系,从数组中提取值,然后将值赋值给变量。
let [a,b,c] = [1,2,3] console.log(a) console.log(b) console.log(c) //控制台输出:1 2 3
当变量没有值可以赋的时候,为undefined
对象解构允许我们使用变量的名字匹配对象的属性,匹配成功将对象属性的值,赋值给变量。
let student = {'name':'liuyu','age':22,'addr':'上海'} //ES6之前的方法 student.name student.age ..... let {name,age,addr} = student console.log(name) console.log(age) console.log(addr) //控制台输出:liuyu 22 上海
第二种写法
let student = {'name':'liuyu','age':22,'addr':'上海'} let {name:stuName,age:stuAge,addr:stuAddr} = student console.log(stuName) console.log(stuAge) console.log(stuAddr)
- { 用于匹配 : 接收的变量 }
- 相比于第一种,第二种方法可以自定义接收值的变量名。
9.4 箭头函数
固定语法:
-
(参数) => {函数体}
代码示例:
let sum = (num1,num2) => {
return num1+num2
}
console.log(sum(1,6))
//控制台输出:7
当函数体中只有一句代码,且代码的执行结果就是返回值,可以省略大括号和return。
let sum = (num1,num2) => num1+num2
console.log(sum(1,6))
//控制台输出:7
当形参只有一个时,小括号可以省略
let sum = num => num * 10
console.log(sum(10))
//控制台输出:100
this指向
- 箭头函数不绑定this,箭头函数没有自己的this关键字。
- 如果在箭头函数中使用this,this关键字将指向函数箭头函数定义位置中的this
- 简单来说就是,箭头函数写在哪个区域,就以哪个区域的this为准
代码示例:
let obj = {'msg':'测试'}
function fun() {
console.log(this) //this为obj
return () => {
console.log(this) //由于写在fun区域,所以this也是obj
}
}
let test = fun.call(obj) //接收函数返回的箭头函数
test() //运行箭头函数
//控制台输出:{msg: '测试'} {msg: '测试'}
面试题
let test = {'a':20,'b':()=>{console.log(this.a)}}
test.b()
//控制台输出:undefined
- 因为对象没有块级作用域的,b方法呢实际上是被定义在了全局作用域上。
- 所以b方法中this指向的是windows,windows.a方法不存在,所以报错。
9.5 剩余参数args
关键字:
...args
...theArgs
作用:
-
当不确定会传入多少参数时,可以利用…theArgsh或者…args接收剩余的参数。
-
特点:以列表的形式接收剩余所有参数
const count = (...theArgs) => { console.log(theArgs) } count(1,2,3,4) //控制台输出:[1, 2, 3, 4] const count = (...args) => { console.log(args) } count(1,2,3,4) //控制台输出:[1, 2, 3, 4]
示例代码:
function sum(...theArgs) {
console.log(theArgs)
//[1, 2, 3, 4, 10]
let total = 0;
for (const arg of theArgs) {
total += arg;
}
return total;
}
console.log(sum(1, 2, 3, 4, 10));
//控制台输出:20
9.6 扩展运算符
扩展运算符可以将数组或者对象转为用逗号分割的参数序列
示例:
let aaa = [1,2,3,4] console.log(...aaa) //控制台输出: 1 2 3 4 // 这里没有用逗号分割是因为console.log将逗号视为连续打印符号了 //等价于: console.log(1,2,3,4)
扩展运算符与解构赋值配合使用
let [a,...b] = arr1 console.log(a) console.log(b) //控制台输出: // 1 // [2, 3]
应用
扩展运算符合并数组-1
let arr1 = [1,2,3] let arr2 = [4,5,6] let arr = [...arr1,...arr2] console.log(arr) //控制台输出: [1, 2, 3, 4, 5, 6]
利用push方法完成数组合并
let arr1 = [1,2,3] let arr2 = [4,5,6] let arr = [] arr.push(...arr1,...arr2) console.log(arr) //控制台输出: [1, 2, 3, 4, 5, 6]
将伪数组变成真正的数组
let divEle = document.getElementsByTagName('div') let divArr = [...divEle]
- 将伪数组变成真正的数组,可以使用数组的所有方法。
sum = num => num * 10
console.log(sum(10))
//控制台输出:100
**this指向**
- 箭头函数不绑定this,箭头函数没有自己的this关键字。
- 如果在箭头函数中使用this,this关键字将指向函数箭头函数定义位置中的this
- 简单来说就是,箭头函数写在哪个区域,就以哪个区域的this为准
代码示例:
```js
let obj = {'msg':'测试'}
function fun() {
console.log(this) //this为obj
return () => {
console.log(this) //由于写在fun区域,所以this也是obj
}
}
let test = fun.call(obj) //接收函数返回的箭头函数
test() //运行箭头函数
//控制台输出:{msg: '测试'} {msg: '测试'}
面试题
let test = {'a':20,'b':()=>{console.log(this.a)}}
test.b()
//控制台输出:undefined
- 因为对象没有块级作用域的,b方法呢实际上是被定义在了全局作用域上。
- 所以b方法中this指向的是windows,windows.a方法不存在,所以报错。
9.5 剩余参数args
关键字:
...args
...theArgs
作用:
-
当不确定会传入多少参数时,可以利用…theArgsh或者…args接收剩余的参数。
-
特点:以列表的形式接收剩余所有参数
const count = (...theArgs) => { console.log(theArgs) } count(1,2,3,4) //控制台输出:[1, 2, 3, 4] const count = (...args) => { console.log(args) } count(1,2,3,4) //控制台输出:[1, 2, 3, 4]
示例代码:
function sum(...theArgs) {
console.log(theArgs)
//[1, 2, 3, 4, 10]
let total = 0;
for (const arg of theArgs) {
total += arg;
}
return total;
}
console.log(sum(1, 2, 3, 4, 10));
//控制台输出:20
9.6 扩展运算符
扩展运算符可以将数组或者对象转为用逗号分割的参数序列
示例:
let aaa = [1,2,3,4] console.log(...aaa) //控制台输出: 1 2 3 4 // 这里没有用逗号分割是因为console.log将逗号视为连续打印符号了 //等价于: console.log(1,2,3,4)
扩展运算符与解构赋值配合使用
let [a,...b] = arr1 console.log(a) console.log(b) //控制台输出: // 1 // [2, 3]
应用
扩展运算符合并数组-1
let arr1 = [1,2,3] let arr2 = [4,5,6] let arr = [...arr1,...arr2] console.log(arr) //控制台输出: [1, 2, 3, 4, 5, 6]
利用push方法完成数组合并
let arr1 = [1,2,3] let arr2 = [4,5,6] let arr = [] arr.push(...arr1,...arr2) console.log(arr) //控制台输出: [1, 2, 3, 4, 5, 6]
将伪数组变成真正的数组
let divEle = document.getElementsByTagName('div') let divArr = [...divEle]
- 将伪数组变成真正的数组,可以使用数组的所有方法。