title: JS高级基础
categories: Javascript
tag: JS
date: 2021-09-25 16:16:34
JS 高级
类和对象
对象
对象是由属性和方法组成的,特指某一个
- 属性:表示事物的特征,在对象中用属性来表示
- 方法:表示对象的行为,在对象中用方法来表示
类
抽出对象的公共部分,泛指某一大类。
类和对象举例
- 通过 class 关键字创建类,类型习惯性首字母大写
- 类里面有一个 constructor 函数,可以接受传递过来的参数,同时返回实例对象
- constructor 函数只要 new 生成实例时,就会自动调用这个函数,如果我们不写这个函数,类也会自动生成这个函数
- 生成实例的 new 不能省略
- 创建类的时候,类名后面不要加小括号。生成实例,类名后面要加小括号,构造函数不需要加 function
class Star {
//1创建类
constructor(uname) {
this.uname = uname
}
}
// 2利用类创建对象
var donghuan = new Star('董欢')
console.log(donghuan.uname)
在类里面加方法
- 在类里面的所有的函数不需要写 function
- 多个函数方法之间不需要添加逗号间隔
class Star {
//1创建类
constructor(uname) {
this.uname = uname
}
sing(song) {
console.log(this.uname + song)
}
}
// 2利用类创建对象
var donghuan = new Star('董欢')
console.log(donghuan.uname)
donghuan.sing('哈哈哈哈')
类的继承
继承 extends
现实中的继承,子承父业。
程序中的继承,子类可以继承父类的一些方法和属性
class Father {
//1创建类
constructor() {}
money() {
console.log(100)
}
}
class Son extends Father {}
let son = new Son()
son.money()
super 关键字
super 关键字可以访问和调用对象父类上的函数,可以调用父类的构造函数,也可以调用父类的普通函数
继承中的属性或者方法查找原则:就近原则
- 继承中,如果实例化子类输出一个方法,先看子类有没有这个方法,如果有,就先执行子类
- 继承中,如果子类里面没有这个方法,就去查找父类有没有这个方法,如果有,就执行父类这个方法
- 调用构造函数
class Father {
constructor(x, y) {
this.x = x
this.y = y
}
sum() {
console.log(this.x + this.y)
}
}
class Son extends Father {
constructor(x, y) {
super(x, y)
}
}
let son = new Son(1, 2)
son.sum()
- 调用普通函数
class Father {
say() {
console.log('我是father')
}
}
class Son extends Father {
say() {
super.say()
}
}
let son = new Son()
son.say()
- 子类继承父类方法同时扩展自己方法
利用 super 调用父类的构造函数,super 必须在子类 this 之前调用
class Father {
constructor(x, y) {
this.x = x
this.y = y
}
sum() {
console.log(this.x + this.y)
}
}
class Son extends Father {
constructor(x, y) {
super(x, y)
this.x = x
this.y = y
}
substract() {
console.log(this.x - this.y)
}
}
let son = new Son(5, 2)
son.sum() //7
son.substract() //3
总结:
- 由于类没有变量提升
- 所以必须先定义类,才能通过类实例化对象
- 类里面的共有属性和方法一定要加 this 使用
类里面 this 的指向问题
- constructor 里面的 this 指向实例对象,方法里面的 this 指向这个方法的调用者
let that
let that_
class Father {
constructor(uname, age) {
console.log(this) //Father {}
that = this
this.uname = uname
this.age = age
this.btn.onclick = this.sing
}
sing() {
//这个sing里面的this指向的是btn这个按钮
//因为这个按钮调用了这个函数
console.log(this) //<button></button>
console.log(this.uname)
}
dance() {
// 这个dance里面指向的this是实例对象donghuan。
//因为donghuan调用了这个函数
that_ = this
console.log(this)
}
}
let donghuan = new Father('董欢')
console.log(that === donghuan) //true
donghuan.dance()
console.log(that_ === donghuan) //true
那么怎么让 sing()指向实例化的对象呢?可以看到 that 是全局变量,我们可以在 sing 函数里面把**console.log(this.uname)**
改为**console.log(that.uname)**
面向对象案例 Tab 栏
初始化
只要这个类实例化,就会调用 constructor 里面的方法.因此直接在 constructor 里面给元素绑定点击事件
class Tab {
constructor(id) {
this.main = document.getElementById(id)
this.lis = this.main.querySelectorAll('li')
this.sections = this.main.querySelectorAll('section')
this.init()
}
init() {
for (let i = 0; i < this.lis.length; i++) {
this.lis[i].index = i
this.lis[i].onclick = function () {}
}
}
//1切换功能
toggleTab() {}
//2添加功能
addTab() {}
//3删除功能
removeTab() {}
//3修改功能
editTab() {}
}
//实例化
new Tab('#tab')
切换功能
//1切换功能
toggleTab() {
/**
*注意事项:
*1. 由于在这个函数中的this是li元素,不具备lis和sections元素
*2. 所以我们应该声明一个全局变量that保存指向的实例化的这个对象
*3. 然后在这个函数中调用lis和sections
*/
// 先清除样式
that.clearClass()
//让点中的这个类,隐藏下边框
this.className = 'liactive'
that.sections[this.index].className = 'conactive'
}
添加功能
- 点击
+
可以实现添加新的选项卡和内容 - 第一步,创建新的选项卡 li 和新的内容 section
- 然后把创建的两个元素追加到对应的父元素中
利用的是**insertAdjacentHTML()**
方法'beforebegin'
:元素自身的前面。'afterbegin'
:插入元素内部的第一个子节点之前。'beforeend'
:插入元素内部的最后一个子节点之后。'afterend'
:元素自身的后面。
注意:由于我们新加入的 li 和 section 并没有被获取为 lis 和 sections 的子集。因此我们就把获取元素重新设为一个函数
init() {
//init初始化操作让相关元素绑定事件
this.updateNode()
this.add.onclick = this.addTab
for (let i = 0; i < this.lis.length; i++) {
this.lis[i].index = i
this.lis[i].onclick = this.toggleTab
}
}
//获取所有li和section
updateNode() {
this.lis = this.main.querySelectorAll('li')
this.sections = this.main.querySelectorAll('section')
}
//2添加功能
addTab() {
//首先把兄弟li和兄弟section取消样式
that.clearClass()
//1. 创建li元素和section元素
var li = '<li class="liactive">新选项卡</li>'
var section = '<section class="conactive>新内容</section>'
//2. 把创建的li和section追加到对应的父元素
//注意:因为addTab是按钮调用的,所以我们要用that
that.ul.insertAdjacentHTML('beforeend', li)
that.fsection.insertAdjacentHTML('beforeend', section)
this.init()
}
删除功能
//获取所有li和section
// 因为我们动态添加元素,会获取所有的li,所以li和关闭按钮个数一致
updateNode() {
this.lis = this.main.querySelectorAll('li')
this.sections = this.main.querySelectorAll('section')
//拿到所有删除按钮
this.remove = this.main.querySelectorAll('.icon-guanbi')
}
//3删除功能
removeTab(e) {
//由于点击删除按钮,但是父元素的li也有点击事件,我们不希望这样
// 所以就阻止冒泡,返回li的点击切换事件
e.stopPropagation()
// 虽然删除按钮没有索引号,但是删除按钮的父元素li有
var index = this.parentNode.index
// 用remove可以直接删除
that.lis[index].remove()
that.sections[index].remove()
that.init()
}
我们希望:
- 当我们删除的不是一个选定状态的 li 的时候,我们想要原来选定的还是依旧选定
- 当我们删除了一个选定状态的 li 的时候,想要它的前一个 li 赋予选定状态
//3删除功能
removeTab(e) {
//由于点击删除按钮,但是父元素的li也有点击事件,我们不希望这样
// 所以就阻止冒泡,返回li的点击切换事件
e.stopPropagation()
// 虽然删除按钮没有索引号,但是删除按钮的父元素li有
var index = this.parentNode.index
// 用remove可以直接删除
that.lis[index].remove()
that.sections[index].remove()
that.init()
//当我们不是删除的选定状态的li的时候,原来的选定状态的li保持不变就可以了
if (document.querySelector('.liactive')) return //直接return之后下面的代码就不会执行了
//,当我们删除的选定状态的li,我们希望,当我们删除了一个li的时候,想要它的前一个li赋予选定状态
index--
//手动调用我们的点击事件,不需要鼠标触发
// 表示前面为真,就去调用后面这个
that.lis[index] && that.lis[index].click()
}
修改操作
双击事件:dbclick
但是如果双击之后,会默认把文字选中,我们希望不要将文字选中
window.getSelection?window.getSelection().removeAllRanges():document.selection.empty()
当我们双击文字的时候,在里面生成一个文本框,当失去焦点或者按下回车,就把文本框输入的值给原先的元素即可。
init() {
//init初始化操作让相关元素绑定事件
this.updateNode()
this.add.onclick = this.addTab
for (let i = 0; i < this.lis.length; i++) {
this.lis[i].index = i
this.lis[i].onclick = this.toggleTab
//给每一个按钮增加点击关闭事件
this.remove[i].onclick = this.removeTab
//给每一个span添加双击事件。双击之后就编辑这个tab
this.spans[i].ondbclick = this.editTab
//给每一个section添加双击事件。双击之后就编辑这个tab
this.sections[i].ondbclick = this.editTab
}
}
//3修改功能
editTab() {
//双击禁止选定文字
window.getSelection ?
window.getSelection().removeAllRanges() :
document.selection.empty()
// 因为这里是在span中添加,所以直接是this,不使用that
//保存以前tab中的文字
var str = this.innerHTML
this.innerHTML = '<input type="text">'
var input = this.children[0]
//让文本框中出现之前的文字
input.value = str
input.select() //让文本框中的文字处于选定状态
//当我们离开文本框,就把文本框里面的值给span
input.onblur = function() {
this.parentNode.innerHTML = this.value
}
//按下回车也可以把文本框里面的值给span
input.onkeyup = function() {
if (e.keyCode === 13) {
//手动调用鼠标点击事件
this.blur()
}
}
}
好了,以上就是面向对象的例子,已经完成了。
构造函数和原型
构造函数和原型
构造函数
构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总与 new 一起使用,我们可以把对象中一些公共属性和方法抽取出来,然后封装在这个函数里面。
new 在执行时会做 4 件事情
- 在内存中创建一个新的对象
- 让 this 指向这个新的对象
- 执行构造函数里面的代码,给这个新对象添加属性和方法
- 返回这个新对象(所以构造函数里面不需要 return)
动态(实例)成员
实例成员就是构造函数内部通过 this 添加的成员,uname,age,sing 就是实例成员
实例成员只能通过实例化的对象来访问
function Star(uname, age) {
//uname,age,sing就是实例成员
this.uname = uname
this.age = age
this.sing = function () {
console.log('我会唱歌')
}
}
var dh = new Star('董欢', 18)
console.log(dh.uname) //通过实例化的对象
console.log(Star.uname) //比如这个:不可以通过构造函数来访问实例成员
静态成员
在构造函数本身添加的成员
// 静态成员表示的是,直接在构造函数上添加的成员,sex就是静态成员
Star.sex = '女'
console.log(Star.sex) //静态成员只能通过对象来访问
构造函数的问题
当构造函数里面有函数的话,当我们实例化一个对象,就得重新开辟一个空间再去放对应的函数,存在浪费内存的问题。
按道理,我们使用的是同一个函数。所以就引入了下面的 prototype
prototype 原型
构造函数通过原型分配的函数就是所有对象所共享的
JS 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象,注意这个 prototype 就是一个对象,这个对象的所有属性和方法都会被构造函数所拥有。
我们可以把那些不变的方法,直接定义到原型对象上,这样所有的对象的实例就可以共享这些方法
Star.prototype.sing = function () {
console.log('我会唱歌')
}
所以此时这个地方就是 true 啦。
console.log(dh.sing() == gwk.sing()) //true
1. 原型是什么?
一个对象,我们也称prototype为原型对象
2. 原型的作用是什么?
共享方法
一般情况下,我们的公共属性定义到构造函数里面,公共的方法我们放到原型对象里面
对象原型__proto__
对象都会有一个属性 proto ,指向构造函数的 prototype 原型对象,之所以我们我们的对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 proto 原型的存在。
console.log(dh.__proto__ === Star.prototype) //true
因此方法查找规则:
- 先看 dh 对象上是否有 sing 方法,如果有就执行这个对象上的 sing
- 如果没有 sing 这个方法,因为有
**__proto__**
的存在,就去构造函数原型对象身上去找 sing 这个方法。
constructor 构造函数
对象原型(__proto__
)和构造函数(prototype
)原型对象里面都有一个属性,constructor 属性,constructor 我们称为构造函数,因为它指回构造函数本身。
console.log(Star.prototype.constructor) //是构造函数本身
console.log(dh.__proto__.constructor) //是构造函数本身
constructor 主要用于记录该对象引用于那个构造函数,它可以让原型对象重新指向原来的构造函数
//很多情况下,我们需要手动的利用constructor这个属性指回原来的构造函数
//当我们使用对象,新添加的对象会把原来的prototype覆盖掉
Star.prototype = {
constructor: Star,
sing: function() {
console.log('我会唱歌')
},
movie: function() {
console.log('我会拍电影')
}
构造函数和实例和原型对象三者的关系
构造函数里面都会有原型对象
- 是通过
prototype
指向原型对象prototype
, - 原型对象里面有一个
constructor
指回了构造函数。 - 原型对象里面也有
__proto__
指向的是Object
构造函数的原型对象prototype
- 而
Object
的原型对象prototype
通过constructor
指向的是Object
构造函数 Object
的原型对象prototype
自然也有__proto__
,这时候指向的是 null。
当我们 new 了一个构造函数就会创建一个实例对象。
- 在这个实例里面有一个对象原型
__proto__
指向原型对象prototype
. - 当然在这个对象实例的
constructor
可以通过原型对象再指回构造函数
原型链
成员查找机制
- 当访问一个对象的属性和方法时,首先查找这个对象自身有没有该属性或者方法
- 如果没有就查找它的原型(也就是
__proto__
指向的prototype
原型对象) - 如果还没有,就查找原型对象的原型(
Object
的原型对象) - 以此类推一直到找到
Object
为止(null) __proto__
对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线- 如果对象本身就有这个属性或者我方法,就不会向上查找。
原型对象 this 指向
- 在构造函数中,里面的 this 指向的对象实例 dh
var dh = new Star('董欢', 18)
- 原型对象里面的 this 也是指向对象实例
Star.prototype.sing = function () {
console.log('wohuichangge ')
that = this
}
dh.sing()
console.log(that === dh) //true
扩展内置对象
可以通过原型对象,对原来的内置对象进行扩展自定义的方法,比如给数组增加求和的功能。
console.log(Array.prototype)
Array.prototype.sum = function () {
let sum = 0
for (let i = 0; i < this.length; i++) {
sum += this[i]
}
return sum
}
var arr = [1, 2, 3]
console.log(arr.sum())
var arr1 = new Array(11, 22, 33)
console.log(arr1.sum())
继承
ES6 之前并没有给我们提供 extends 继承,我们可以通过构造函数+原型对象模拟实现继承,被称为组合继承。
call()
调用这个函数,并且修改函数运行时的 this 指向
fun.call(thisArg, arg1, arg2)
thisArg
:当前调用函数 this 的指向对象arg1
,arg2
表示传递的其它参数
- call 可以调用函数
//1. call可以调用函数
fn.call()
- 可以调用它改变这个函数的 this 指向,此时这个函数的指向就指向了 o.通俗的解释就是打电话叫 o 来调用
function fn() {
console.log('我想喝水')
console.log(this) //window
}
var o = {
name: 'andy'
}
//1. call可以调用函数
fn.call()
//2. call可以改变这个函数的this指向,此时这个函数的指向就指向了o
fn.call(o)
- 传递参数
function fn(x, y) {
console.log('我想喝水')
console.log(this) //window
console.log(x + y) //3
}
var o = {
name: 'andy'
}
//3. 传递参数
fn.call(o, 1, 2)
借用构造函数继承父类型属性
子构造函数继承父构造函数的属性
//1。 父构造函数
function Father(uname, age) {
// this指向父构造函数里面的对象实例
this.uname = uname
this.age = age
}
//2. 子构造函数
function Son(uname, age, score) {
// this指向子构造函数里面的对象
// 调用了父构造函数,然后把子构造函数的this传递给父构造函数
Father.call(this, uname, age)
//也可以自己添加属性
this.score = score
}
var son = new Son('董欢', 18, 100)
console.log(son) //此时son里面有uname,age,score这两个属性
子构造函数继承父构造函数的方法
- (写在原型对象上)
首先声明,这种方法是有问题的。这样子直接赋值的话,如果直接修改了子原型对象,父原型对象也会跟着一起变化
(借用父构造函数继承实际意义并不是真正的继承,只是 this 的指向发生了改变,父构造函数原型上的属性和方法子类都得不到)
要是我们使用父构造函数的方法,就把方法定义在原型上。然后赋值给子构造函数,但是,子构造函数自己的原型对象上的方法虽然不会被覆盖,但是子构造函数的原型对象里的方法也会给父构造函数的原型对象上。
Father.prototype.money = function () {
console.log(10000)
}
//2. 子构造函数
function Son(uname, age, score) {
// this指向子构造函数里面的对象
// 调用了父构造函数,然后把子构造函数的this传递给父构造函数
Father.call(this, uname, age)
//也可以自己添加属性
this.score = score
}
Son.prototype = Father.prototype
子构造函数原型对象引用的父构造函数的原型对象。因此就会修改父构造函数的原型对象。
Son.prototype = Father.prototype
Son.prototype.exam = function () {
console.log('考试')
}
//此时父构造函数里面也有这个方法
console.log(Father.prototype)
- 通过 Father 的实例对象继承父构造函数
Son.prototype = new Father()
//如果利用了对象的形式修改了原型对象,别忘了利用constructor指回原来的对象
Son.prototype.constructor = Son
实例对象和原型对象的地址是不一样的。
类的本质
ES6 之前通过构造函数+原型实现面向对象编程
ES6 通过类实现面向对象编程
类的本质其实还是一个函数,我们也可以简单的认为类就是构造函数的另外一种写法
class Star {}
console.log(typeof Star) //function
那么构造函数有哪些特点呢?
- 构造函数有原型对象
prototype
- 构造函数原型对象
prototype
里面有constructor
指向的构造函数本身 - 构造函数可以通过原型对象添加方法
- 构造函数创建的实例对象有
__proto__
原型指向 构造函数的原型对象
那么类呢?
console.log(typeof Star) //function
//1. 类有原型对象
console.log(Star.prototype)
//2. 类里面原型对象prototype里面有constructor指向的构造函数本身
console.log(Star.prototype.constructor)
//3. 类可以通过原型对象添加方法
Star.prototype.sing = function () {
console.log('冰雨')
}
//4. 构造函数创建的实例对象有__proto__原型指向,构造函数的原型对象
let dh = new Star()
console.log(dh) //Star {}
console.log(dh.__proto__ === Star.prototype) //true
总结:
ES6 的绝大部分功能。ES5 都可以做到,新的 class 写法只是让对象原型的写法更加清晰,更像面向对象编程的语法而已。所以 ES6 的类其实就是语法糖
ES5 新增的方法
ES5 给我们新增了一些方法,可以很方便的操作数组或字符串,这些方法主要包括:
- 数组方法
- 字符串方法
- 对象方法
数组方法
迭代遍历方法
1. forEach()
- currentValue:数组当前项的值
- index:数组当前项的索引
- arr : 数组本身
let arr = [1, 2, 3]
let sum = 0
arr.forEach((value, index, array) => {
console.log('值' + value)
console.log('索引' + index)
console.log(array)
sum += value
})
console.log(sum)
2. map()
map 和 forEach 类似,但是 map 会返回的新数组
let arr = [1, 2, 3, 4]
let result = arr.map((value, index, array) => {
return value + 60
})
console.log(result) //[ 61, 62, 63, 64 ]
3. filter()
filter 是创建一个新数组,新数组中的元素是通过检查指定数组中符合条件的所有元素,主要用于筛选数组。
他直接返回一个新数组,不会影响原来的数组
- currentValue:数组当前项的值
- index:数组当前项的索引
- arr : 数组本身
let arr = [20, 34, 54, 12]
let result = arr.filter((value, index, array) => {
return value > 20
})
console.log(result) //34,54
4. some()
查找数组中是否有满足条件的元素,如果找到了第一个满足条件的元素就终止循环,不再继续查找。
- currentValue:数组当前项的值
- index:数组当前项的索引
- arr : 数组本身
let arr = [20, 34, 54, 12]
let result = arr.some((value, index, array) => {
return value > 60
})
console.log(result) //false
filter 也是查找满足条件的元素,返回的是一个数组,而且是把所有满足条件的元素返回回来
some 也是查找满足条件的元素是否存在,返回的是一个布尔值,、如果查找到了第一个满足条件的元素就不查找了
5. every()
every()表示这个数组必须全部满足条件。查到了第一个符合的,还会继续查找。查找其中一个不符合的,就直接 false
let arr = [1, 2, 3, 4]
let result = arr.every((value, index, array) => {
return value > 3
})
console.log(result) //false
forEach 和 some、filter 区别
forEach 会一直遍历完。some 找到了就不会遍历。
- 在 forEach 里面 return true。forEach 会一直遍历完
- 在 some 里面遇到了 return true。就不会继续遍历
- 在 filter 里面遇上了 return true,也还是会继续遍历完
trim()去除字符串两端空白字符
//去除两边的空格
let str = ' h a '
console.log(str)
let str1 = str.trim()
console.log(str1)
Object.defineProperty 方法
Object.defineProperty(obj, prop, descriptor)
obj
: 必需,目标对象prop
: 必需,需要定义或者修改的属性名字descriptor
: 必需,目标属性所拥有的特性
其中第三个尝试descriptor
说明:应该以对象形式书写
value
:设置属性的值,默认为undefined
writeable
:值是否可以重写,true
|false
enumerable
:目标属性是否可以被枚举,true
|false
,默认为false
configurable
:目标属性是否可以被删除或是否可以再次修改特性,true
|false
,默认为false
- 关于
value
,表示加入的属性的值
// 也就是给我们的obj新增了一个num属性,他的值为1000
Object.defineProperty(obj, 'num', {
value: 1000
})
console.log(obj) //此时就添加进去了
// 已有属性就是修改
Object.defineProperty(obj, 'price', {
value: 99
})
console.log(obj) //此时就修改完成
writeable
表示是否可被修改
Object.defineProperty(obj, 'id', {
//表示不允许被修改
writable: false
})
obj.id = 2
console.log(obj) //此时id不会发生改变
enumerable
表示是否能被遍历,默认为false
.可以使用Object.keys()
进行遍历
Object.defineProperty(obj, 'address', {
//表示不允许被修改
value: '四川省成都市',
enumerable: false
})
console.log(Object.keys(obj)) // ['id', 'pname', 'price']
configurable
默认为false
如果为false
,则不允许被删除,不允许被修改特性。
Object.defineProperty(obj, 'address', {
//表示不允许被修改
value: '四川省成都市',
enumerable: false,
// 不允许被删除或者重新设定特性
configurable: false
})
函数进阶
1. 函数定义方式
- function 关键字
- 函数表达式
- new Function(). (不推荐)
//1.
function fn() {}
//2.
var fun = function () {}
//3.
var f = new Function('a', 'b', 'console.log(a+b)')
f(1, 2)
因此,所有的函数都是 Function 的实例(对象)
所以函数也属于是是对象
console.dir(f instanceof Object) //true
2. 函数调用
6 种函数调用的方法
//1. 调用普通函数
function fn() {
console.log('调用普通函数')
}
fn.call()
//2. 调用对象里面的方法
var o = {
sayHi: function () {
console.log('调用对象')
}
}
o.sayHi()
//3. 调用构造函数
function Star() {}
new Star()
//4. 绑定事件函数
btn.onclick = function () {} //点击了按钮就可以调用
//5. 定时器调用
//这个是定时器隔一分钟自动调用
setInterval(function () {}, 1000)(
//6. 立即执行函数
function () {
console.log('自动调用')
}
)()
3 this 指向
6 种函数的 this 指向,因为调用方式不同,决定了 this 指向的不同
//1. 调用普通函数
// 普通函数的this指向window
function fn() {
console.log('调用普通函数')
//指向window
console.log(this)
}
fn()
//2. 调用对象里面的方法
// 对象里面的this指向对象o
var o = {
sayHi: function () {
console.log('调用对象')
}
}
o.sayHi()
//3. 调用构造函数
// 构造函数里面的this指向实例对象dh
// 原型对象指向的也是实例对象
function Star() {}
// 原型对象指向的也是实例对象
Star.prototype.sing = function () {}
let dh = new Star()
//4. 绑定事件函数
// 指向btn这个函数调用者
btn.onclick = function () {} //点击了按钮就可以调用
//5. 定时器调用
//这个是定时器隔一分钟自动调用
// 定时器里面指向的是window
setInterval(function () {}, 1000)(
//6. 立即执行函数
// this指向的是window
function () {
console.log('自动调用')
}
)()
调用方式 | this 指向 |
---|---|
普通函数调用 | window |
构造函数调用 | 实例对象,原型对象里面的方法也指向实例对象 |
对象方法调用 | 该方法所属对象 |
事件绑定方法调用 | 绑定事件对象 |
定时器函数 | window |
立即执行函数 | window |
4 改变 this 的指向
call()、apply()、bind()
- call()方法
- call()可以调用函数和改变 this 的指向。
- call 的主要作用可以实现继承
function Father(uname, age) {
this.uname = uname
this.age = age
}
function Son(uname, age) {
Father.call(this, uname, age)
}
let son = new Son('董欢', '女')
console.log(son)
- apply()方法
apply()方法调用一个函数,简单理解为调用函数的方式,但是它可以改变 this 的指向
fun.apply(thisArg, [argArray])
- this.Arg:在 fun 函数运行时指定的 this 值
- argsArray:传递的值,必需包含在数组里面
- 返回值就是函数的值,因为它就是调用函数
apply 和 call 的区别就是,apply 接收的是数组形式。
var o = {
name: 'andu'
}
function fn(arr) {
console.log(this)
console.log(arr) //dh
}
fn.apply(o, ['dh'])
apply 的主要应用
- 我么可以利用 apply 借助于数学内置对象求最大值
var arr = [1, 66, 3, 99]
var max = Math.max.apply(Math, arr)
var min = Math.min.apply(Math, arr)
console.log(max, min)
- bind()方法
bind 不会调用函数,但是能改变函数内部 this 指向
fun.bind(thisArg, arg1, arg2)
- thisArg:在 fun 函数运行时指定的 this 值
- arg1,arg2 传递的其他参数
- 返回由指定的 this 值和初始化参数改造的原函数拷贝
var f = fn.bind(o)
f()
在开发中,我们经常使用 bind.
因为我们有的函数我们不需要立即调用,但是又想改变这个函数内部的 this 指向,此时用 bind。
- 比如我们有一个按钮,当我们点击了之后就禁用这个按钮,3 秒钟之后开启这个按钮。但是定时器里面的 this 是 window。所以我们需要改变
let btn = document.querySelector('button')
btn.onclick = function () {
this.disabled = true //这个this指向的btn
setTimeout(
function () {
//如果没有加bind更改指向的话就不可以这样写,定时器里面的this指向的是window。没有disabled这个属性
this.disabled = false
//我们想要this指向btn,又不想让他们立即调用,所以使用bind
}.bind(btn),
3000
)
}
- 当有多个按钮时.通过 bind 就可以实现对应的按钮点击事件了。
let btns = document.querySelectorAll('button')
for (let i = 0; i < btns.length; i++) {
btns[i].onclick = function () {
this.disabled = true //这个this指向的btn
setTimeout(
function () {
this.disabled = false
}.bind(this), //这个this就是指向的对应的btn
3000
)
}
}
bind()应用
之前做 tab 栏案例的时候,我们用的全局变量 that,通过 bind 我们可以把大一点的 this 通过参数传递,而函数本身的 this 不变就可以啦。
call、apply、bind 总结
相同点:
都可以改变 this 的指向
区别点:
- call 和 apply 会调用函数,并且会改变函数内部 this 指向
- call 和 apply 传递的参数不一样,call 传递参数 arg1,arg2 形式,aplly 必需时数组形式[arg1,arg2]
- bind 不会调用函数,可以改变 this 指向
主要应用场景
- call 经常做继承
- apply 经常和数组有关系
- bind 不调用函数,还想改变函数内部 this 指向
严格模式
什么是严格模式?
ES5 的严格模式是采用具有限制性 JS 变体的一种方式,即在严格的条件下运行 JS 代码。
做了以下更改
- 消除了 JS 语法的一些不合理、不严谨之处,减少了一些怪异行为。
- 消除了代码运行的不安全之处
- 提高编译器效率,增加运行速度
- 禁用了保留字做变量,比如 class,enum,export, extends,import 等不能做变量名
开启严格模式
严格模式可以应用到整个脚本或者个别函数中,因此在使用时,我们可以把严格模式分为为脚本开启严格模式和为函数开启严格模式
- 为脚本开启严格模式
<script>'use strict'; // 下面的JS代码就会按照严格模式执行代码</script>
- 为函数开启严格模式
function fn() {
'use strict'
//下面的代码按照严格模式执行
}
function fun() {
//里面的代码还是按照普通模式执行
}
严格模式的变化
- 变量必需先声明再使用
- 我们不能删除声明好的变量
- 严格模式下的函数里面的 this 是 undefined,而不是 window
function fn() {
console.log(this) //undefined
}
fn()
- 定时器里面的 this 还是指向 window
- 事件。对象还是指向调用者
高阶函数
高阶函数是对其他函数进行操作的函数,它接受函数作为参数或将函数作为返回值输出
- 以函数作用参数
function fn(a, b, callback) {
console.log(a + b)
callback && callback()
}
fn(1, 2, function () {
console.log('我是最后调用的')
})
/**调用结果
* 3
我是最后调用的
*/
- 以函数作为返回值
function fn() {
return function () {}
}
fn()
闭包
变量作用域
变量根据作用域的不同分为两种,全局变量和局部变量
- 函数内部可以使用全局变量
- 函数外部不可以使用局部变量
- 当函数执行完毕,本作用域内的局部变量会销毁
什么是闭包
闭包是指一个函数有权访问另一个函数作用域中变量,变量所在的函数
// 我们fun函数这个作用域访问了另一个函数fn里面的局部变量num,变量所在的函数就是闭包,fn就是闭包
function fn() {
let num = 10
function fun() {
console.log(num)
}
fun()
}
fn()
闭包的作用
我们 fn 外面的作用域可以访问 fn 内部的局部变量
function fn() {
let num = 10
function fun() {
console.log(num)
}
return fun
}
var f = fn()
//相当于
// var f = function fun() {
// console.log(num)
// }
f() //10
闭包的主要作用,延伸了变量的作用范围。(孩子偷了爸爸的变量给外人用)
闭包案例
案例 1. 点击 li 就打印相对应的索引
for (var i = 0; i < lis.length; i++) {
lis[i].index = i
lis[i].onclick = function () {
// for循环是个同步任务,会立马执行,所以i是4,
//当我们点击小li的时候,for循环已经执行完了。所以都是4
console.log(i) //4
}
}
点击 li 输出当前 li 的索引号.此时这个立即函数就是闭包,因为 onclick 后执行的函数用了立即执行函数里面的变量 i
//利用闭包的方式得到当前小li的索引号
for (var i = 0; i < lis.length; i++) {
// 利用for循环创建了4个立即执行函数
;(function (i) {
lis[i].onclick = function () {
console.log(i)
}
})(i)
}
闭包会有内存泄漏的问题
案例 2. 3 秒之后打印 li 里面的内容,for 是同步任务,计时器是异步任务。
for (var i = 0; i < lis.length; i++) {
;(function (i) {
setTimeout(function () {
console.log(lis[i].innerHTML)
}, 3000)
})(i)
}
案例 3. 计算打车价格
此时这个立即执行函数是闭包
// 打车起步价13元(3公里内),之后每多一公里增加5元,用户输入公里数就可以计算打车价格
//如果有拥堵情况,总价格多收10元拥堵费
var car = (function () {
var start = 13 //起步价
var total = 0 //总价
return {
//正常的总价
price: function (n) {
if (n <= 3) {
total = start
} else {
total = start + (n - 3) * 5
}
return total
},
//拥堵之后的费用
yd: function (flag) {
return flag ? total + 10 : total
}
}
})()
console.log(car.price(5)) //23
console.log(car.yd(true)) //33=23+10
console.log(car.price(1)) //13
console.log(car.yd(true)) //23=13+10
思考题 1(没有闭包产生)
var name = 'the window'
var object = {
name: 'my Object',
getName: function () {
return function () {
return this.name
}
}
}
console.log(object.getName()()) //donghuan
var f = object.getName()
// 类似于
var f = function () {
return this.name
}
f()(
/*
要注意的是this指向谁?
this在匿名函数里面。
再执行匿名函数
匿名函数的this指向的是window
所以打印的是the window
*/
function () {
this
}
)()
思考题 2(有闭包产生)
var name = 'the window'
var object = {
name: 'my Object',
getName: function () {
var that = this
return function () {
return that.name
}
}
}
console.log(object.getName()()) //my Object
// 相当于
//this指向调用者。object,
//然后我们用that存储了object
var f = object.getName()
var f = function () {
return that.name //that指向object,所以打印的是my Object
}
f()
闭包总结
- 闭包是什么
闭包是一个函数,能够被其他作用域访问的变量所在的函数 - 闭包的作用
扩大作用域的范围
递归(必须有 return)
如果一个函数内部可以调用其自身,那这个函数就是递归函数。简单理解:函数内部自己调用自己,这个函数就是递归函数。
递归函数的作用和循环效果一样
递归很容易发生”栈溢出”错误,因为不断的调用自己,调着调着自己就爆了。所以必须退出条件 return
//递归函数:函数内部自己调用司机,这个函数就是递归函数
let num = 1
function fn() {
console.log('我要打印6句话')
if (num == 6) {
return //递归里面必须加退出条件
}
num++
fn()
}
fn()
1. 利用递归求数学题
- 求
1*2*3*...*n
阶乘
// 利用递归函数求1 - n的阶乘1 * 2 * 3 * 4...*n
function fn(n) {
if (n == 1) {
return 1
}
return n * fn(n - 1)
}
// fn(3)
console.log(fn(5))
//详细思路 假如用户输入的是3
// return 3*fn(2)
// return 3 * (2 * fn(1))
// return 3*2*1
- 求斐波拉契数列
// 我们只需要知道用户输入的n和前面的两项(n-1,n-2)就可以计算出n对应的序列值
function fn(n) {
if (n == 1 || n == 2) {
return 1
}
return fn(n - 1) + fn(n - 2)
}
console.log(fn(4))
2. 利用递归遍历数据
- 我们想要做输入 id 号,就可以返回的数据对象
// 我们想要做输入id号,就可以返回的数据对象
const data = [
{
id: 1,
name: '家电',
goods: [
{
id: 11,
gname: '冰箱'
},
{
id: 12,
gname: '洗衣机'
}
]
},
{
id: 2,
name: '服饰'
}
]
// 1. 利用forEach去遍历立main的每一个对象
function getID(json, id) {
let result = {}
json.forEach(function (item) {
if (item.id == id) {
// console.log(item)
result = item
}
// 2. 里面应该有goods这个数组,并且数组长度不为0
else if (item.goods && item.goods.length > 0) {
result = getID(item.goods, id)
}
})
return result
}
console.log(getID(data, 11))
console.log(getID(data, 12))
浅拷贝和深拷贝
- 浅拷贝只是拷贝一层,更深层次对象级别的只拷贝引用
- 深拷贝拷贝多层,每一级别都会拷贝
浅拷贝
var obj = {
id: 1,
name: 'andy',
msg: {
age: 18
}
}
var o = {}
//这是浅拷贝。对于msg属性只是拷贝了地址
// 浅拷贝第一种
for (var k in obj) {
// k是属性名,obj[k]是属性值
o[k] = obj[k]
}
console.log(o.message == obj.message) //true
console.log(o)
//浅拷贝第二种
console.log('----------------------')
Object.assign(o, obj)
console.log(o)
console.log(o.message == obj.message) //true
深拷贝(数组,对象)
使用递归的方式实现深拷贝
在这里,数组要放在前面,因为数组也属于对象。
// 封装函数
function deepCopy(newObj, oldObj) {
for (var k in oldObj) {
//1. 获取我们的属性值是简单的还是复杂的
var item = oldObj[k]
//2. 判断这个值是否是数组
if (item instanceof Array) {
newObj[k] = []
deepCopy(newObj[k], item)
//3. 判断是否是对象
} else if (item instanceof Object) {
newObj[k] = {}
deepCopy(newObj[k], item)
}
//4. 如果不是数组也不是对象,就是简单数据类型
else {
newObj[k] = item
}
}
}
deepCopy(o, obj)
console.log(o)
console.log(o.msg === obj.msg) //false
正则表达式
什么是正则表达式
是用于匹配字符串中的字符组合的模式,在 JS 中,正则表达式也是对象
通常被用来检索,替换那些符合某个模式(规则)的文本,例如验证表单,用户名表单只能输入英文字母,数字或者下划线,昵称输入框中可以输入中文(匹配)。此外,正则表达式还常用于过滤掉页面内容中的一些敏感词(替换),或者从字符串中获取我们想要的特定部分(提取)等。
正则表达式的特点
- 灵活性、逻辑性和功能性非常强
- 可以迅速以极简单的方式达到字符串的复杂控制
- 对于刚接触的人来说,比较晦涩难懂
- 实际开发,一般都是直接复制好已经写好的正则表达式,但是要求会使用正则表达式并且根据实际情况修改
正则表达式在 JS 中的使用(待。。。。。)
1. 利用 RegExp 对象来创建正则表达式
var regexp = new RegExp(/123/)
console.log(regexp)
2. 利用字面量创建
var rg = /123/
测试正则表达式 test
test()正则对象方法。用于检测字符串是否符合该规则,该对象会返回 true 或者 false,其参数的需要测试的字符串
检测123是否符合rg
var rg = /123/
console.log(rg.test(123)) //true符合