JS进阶学习
作用域&解构&箭头函数
作用域
- 作用域(scope)规定了变量能够被访问的“范围”,离开了这个“范围”变量便不能被访问,
- 作用域分为:
- 局部作用域
- 全局作用域
局部作用域
局部作用域分为函数作用域和块作用域。
-
函数作用域:在函数内部声明的变量只能在函数内部被访问,外部无法直接访问。
<script> function getSum() { // 函数内部是函数作用域 属于局部变量 const num = 10 console.log(num) // 此处报错 函数外部不能使用局部作用域变量 } </script>
- 函数内部声明的变量,在函数外部无法被访问
- 函数的参数也是函数内部的局部变量
- 不同函数内部声明的变量无法互相访问
- 函数执行完毕后,函数内部的变量实际被清空了
-
块作用域:在JavaScript中使用{}包裹的代码称为代码块,代码块内部声明的变量外部将【有可能】无法被访问。
for(let i = 0; i < 10; i++){ // i只能在该代码块中被访问 console.log(i) // 正常 } // 超出了 i 的作用域 console.log(i) // 报错
- let 声明的变量会产生块作用域,var 不会产生块作用域
- const声明的常量也会产生块作用域
- 不同代码块之间的变量无法互相访问
- 推荐使用 let 或 const
全局作用域
<script>标签和**.js**文件的【最外层】就是所谓的全局作用域,在此声明的变量在函数内部也可以被访问。
全局作用域中声明的变量,任何其它作用域都可以被访问
<script>
// 全局作用域
// 全局作用域下声明了 num 变量
const num = 10
function fn() {
//函数内部可以使用全局作用域的变量
console.log(num)
}
//此处全局作用域
</script>
- 为 window 对象动态添加的属性默认也是全局的,不推荐!
- 函数中未使用任何关键字声明的变量为全局变量,不推荐!
- 尽可能少的声明全局变量,防止全局变量被污染
作用域链
作用域链本质上是底层的变量查找机制。
- 在函数被执行时,会优先从当前函数作用域中查找变量
- 如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域
- 嵌套关系的作用域串联起来形成了作用域链
- 相同作用域链中按着从小到大的规则查找变量
- 子作用域能够访问父作用域,父级作用域无法访问子级作用域
JS垃圾回收机制
垃圾回收机制(Garbage Collection)简称GC
JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收
- 内存的生命周期:JS环境中分配的内存,一般有如下生命周期:
- 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
- 内存使用:即读写内存,也就是使用变量、函数等
- 内存回收:使用完毕,由垃圾回收器自动回收不再使用的内存
- 说明:
- 全局变量一般不会回收(关闭页面回收)
- 一般情况下局部变量的值,不用了,会被自动回收
- 内存泄漏:程序中分配的内存由于某种原因程序未释放或无法释放叫做内存泄漏
拓展-JS垃圾回收机制-算法说明
堆栈空间分配区别:
- 栈(操作系统) :由操作系统自动分配释放函数的参数值、局部变量等,基本数据类型放到栈里面。
- 堆(操作系统):一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收。复杂数据类型放到堆里面。
两种常见的浏览器垃圾回收算法:引用计数法 和 标记清除法
- 引用计数法:
- IE采用的引用计数算法,定义“内存不再使用”,就是看一个对象是否有指向它的引用,没有引用了就回收对象
- 算法:
- 跟踪记录被引用的次数
- 如果被引用了一次,那么就记录次数1,多次引用会累加 ++
- 如果减少一个引用就减1 –
- 如果引用次数是0,则释放内存
- 它存在一个致命的问题:嵌套引用(循环引用)
- 如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露。
function fn(){ let o1 = {} let o2 = {} o1.a = o2 o2.a = o1 return '引用计数无法回收' } fn()
- 因为他们的引用次数永远不会是0。这样的相互引用如果说很大量的存在就会导致大量的内存泄露
- 如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露。
- 标记清除法:
- 现代的浏览器已经不再使用引用计数算法了。
- 现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。
- 核心:
- 标记清除算法将“不再使用的对象”定义为“无法达到的对象”。
- 就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。
- 那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
闭包
-
简单理解:闭包=内层函数+外层函数的变量
-
闭包作用:封闭数据,提供操作,外部也可以访问函数内部的变量闭包的
-
基本格式:
function outer() { let i = 1 function fn() { console.log(i) } return fn } const fun = outer() fun() // 1 //外层函数使用内部函数的变量
-
闭包应用:实现数据的私有
- 比如,我们要做个统计函数调用次数,函数调用一次,就++
-
闭包可能引发的问题:内存泄漏
变量提升
变量提升是JavaScript中比较"奇怪”的现象,它允许在变量声明之前即被访问(仅存在于var声明变量)
// 访问变量 str
console.log(str + 'world!')
//声明变量 str
var str = 'hello '
注意:
- 变量在未声明即被访问时会报语法错误
- 变量在var声明之前即被访问,变量的值为 undefined
- let/const声明的变量不存在变量提升
- 变量提升出现在相同作用域当中
- 实际开发中推荐先声明再访问变量
说明:JS初学者经常花很多时间才能习惯变量提升,还经常出现一些意想不到的bug,正因为如此, ES6引入了块级作用域,用let或者const声明变量,让代码写法更加规范和人性化。
函数进阶
函数提升
函数提升与变量提升比较类似,是指函数在声明之前即可被调用。
// 调用函数
fn()
// 声明函数
function fn(){
console.log('声明之前就被调用')
}
// 不存在提升现象
fn() // 错误
var fn = function () {
console.log('表达式不存在提升现象...')
}
总结:
- 函数提升能够使函数的声明调用更灵活
- 函数表达式不存在提升的现象
- 函数提升出现在相同作用域当中
函数参数
-
动态参数:arguments是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参
- arguments 是一个伪数组,只存在于函数中
- arguments的作用是动态获取函数的实参
- 可以通过for循环依次得到传递过来的实参
-
剩余参数:剩余参数允许我们将一个不定量的参数表示为一个数组
-
…是语法符号,置于最末函数形参之前,用于获取多余的实参
-
借助….获取的剩余实参,是个真数组
function config(baseURL, ...other) { console.log(baseURL) // 得到 'http://baidu.com' console.log(other) // other 得到['get', 'json'] } // 调用函数 config('http://baidu.com', 'get', 'json');
-
开发中,还是提倡多使用 剩余参数。
-
-
展开运算符(…),将一个数组进行展开
const arr = [1, 5, 3, 8, 2] console.log(...arr) // 1 5 3 8 2
- 说明:不会修改原数组
- 典型运用场景:求数组最大值(最小值)、合并数组等
const arr1 = [1,2,3] // 展开运算符 可以展开数组 // console.log(...arr) console.log(Math.max(...arr1)) console.log(Math.min(...arr1)) // 合并数组 const arr2 = [3,4,5] const arr = [...arr1,...arr2] console.log(arr)
剩余参数:函数参数使用,得到真数组
展开运算符:数组中使用,数组展开
箭头函数
使用场景:箭头函数更适用于那些本来需要匿名函数的地方
-
基本语法
- 箭头函数属于表达式函数,因此不存在函数提升
- 只有一个参数可以省略小括号
- 函数体只有一行代码可以省略大括号,且无需return,直接返回
- 箭头函数可以直接返回一个对象
// // 箭头模式 // const fn = (x)=>{ // console.log(x) // } // fn(123) // // 只有一个参数可以省略小括号 // const fn = x => { // console.log(x) // } // fn(123) // // 只有一个行代码可以省略大括号 // const fn = x => console.log(x) // fn(123) const form = document.querySelector('form') form.addEventListener('click', ev => ev.preventDefault()) const fn = (uname) => ({uname:uname}) console.log(fn('jj'))
-
箭头函数参数
- 普通函数有 arguments 动态参数
- 箭头函数没有 arguments 动态参数,但是有剩余参数 …args
// 利用箭头函数来求和 const getSum = (...arr) => { let sum = 0 for (let i = 0; i < arr.length; i++){ sum += arr[i] } return sum } const result = getSum(1,2,34) console.log(result)
-
箭头函数this
- 在箭头函数出现之前,每一个新函数根据它是被如何调用的来定义这个函数的this值,非常令人讨厌。
- 箭头函数不会创建自己的this,它只会从自己的作用域链的上一层沿用this。
- 在开发中【使用箭头函数前需要考虑函数中this的值】 ,事件回调函数使用箭头函数时, this为全局的window,因此DOM事件回调函数为了简便,还是不太推荐使用箭头函数
// 以前this的指向: 谁调用这个函数,this就指向谁 console.log(this) // window // 普通函数 function fn() { console.log(this) // window } window.fn() // 对象方法里面的this const obj = { name: 'andy', sayHi: function(){ console.log(this) // obj } } obj.sayHi() // 箭头函数的this 时上一层作用域的this指向 const fn = () => { console.log(this); // window } fn() const obj1 = { uname: '只因', sayHi: () => { console.log(this) // this指向window } } obj1.sayHi()
解构赋值
解构赋值是一种快速为变量赋值的简洁语法,本质上仍然是为变量赋值。
分为:
- 数组解构
- 对象解构
数组解构
数组解构是将数组的单元值快速批量赋值给一系列变量的简洁语法。
基本语法:
-
赋值运算符 = 左侧的[]用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量
-
变量的顺序对应数组单元值的位置依次进行赋值操作
const arr = [1,2,3] // 数组结构 赋值 const [one,tuo,three] = arr // const one=arr[0] // const tuo=arr[1] // const three=arr[2] console.log(one) // 1 console.log(tuo) // 2 console.log(three) // 3
-
典型应用交换两个变量的值
- 注意:js前面必须加分号的情况
- 立即执行函数
- 数组结构
(function t () {})(); // 或者 ;(function t() {})() let a=1 let b=2; //数组开头的,提别是前面有语句的一定要注意加分号 [b,a] = [a,b] console.log(a,b)
-
变量多 单元值少的情况:变量的数量大于单元值数量时,多余的变量将被赋值为 undefined
const [a,b,c,d] = [1,2,3] console.log(a) // 1 console.log(b) // 2 console.log(c) // 3 console.log(d) // undefined
-
变量少 单元值多的情况:
const [a,b] = [1,2,3] console.log(a) // 1 console.log(b) // 2
-
利用剩余参数解决变量少 单元值多的情况:
const [a,b,...arr] = [1,2,3,4,5] console.log(a) // 1 console.log(b) // 2 console.log(arr) // [3,4,5]
-
防止有undefined传递单元值的情况,可以设置默认值:允许初始化变量的默认值,且只有单元值为 undefined 时默认值才会生效
const [a = 0, b = 0] = [] console.log(a) // 0 console.log(b) // 0
-
按需导入,忽略某些返回值:
const [a, , c, d] = [1,2,3,4] console.log(a) // 1 console.log(c) // 3 console.log(d) // 4
-
多维数组结构
const [a, b, [c, d]] = [1,2,[3,4]] console.log(a) // 1 console.log(b) // 2 console.log(c) // 3 console.log(d) // 4
对象解构
对象解构是将对象属性和方法快速批量赋值给一系列变量的简洁语法
-
基本语法:
- 赋值运算符=左侧的{}用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量
- 对象属性的值将被赋值给与属性名相同的变量
- 注意解构的变量名不要和外面的变量名冲突否则报错
- 对象中找不到与变量名一致的属性时变量值为 undefined
// 对象解析 const obj = { uname : 'dog', age : 114514 } // obj.uname // obj.age // 解构语法 const {uname, age} = {uname : 'dog', age : 114514} // 等价于 const uname = obj.uname console.log(uname) console.log(age)
-
给新的变量名赋值:
- 可以从一个对象中提取变量并同时修改新的变量名
- 冒号表示“什么值:赋值给谁”
// 对象解构的变量名 可以重新改名 旧:新 const {uname:username,age} = {uname:'pig',age:2333} console.log(username) console.log(age)
-
数组对象解构
const pig = [ { uname:'八戒', age:114514 } ]; const [{uname, age}] = pig; console.log(uname); console.log(age);
-
多级对象解构
const pig = { name: '八戒', family:{ mother: '裴氏夫人', father: '尉迟宗', }, age:114514 } const {name, family: {mother,father}} = pig console.log(name); console.log(mother); console.log(father);
遍历数组forEach方法
- forEach()方法用于调用数组的每个元素,并将元素传递给回调函数
- 主要使用场景:遍历数组的每个元素
- 语法:
被遍历的数组.forEach(function (当前数组元素, 当前元素索引号) {函数体})
- 例如:
const arr = ['pig','dog','cat'] arr.forEach(function(item,index){ console.log(item) // 数组元素 pig dog cat // 依次打印数组的每个元素 console.log(index) // 索引号 })
- 注意:
- forEach主要是遍历数组
- 参数当前数组元素是必须要写的, 索引号可选。
筛选数组filter方法
- filter()方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素
- 主要使用场景:筛选数组符合条件的元素,并返回筛选之后元素的新数组
- 语法:
被遍历的数组.fibter(function (currentValue, index) { return 筛选条 })
- 例:
// 筛选数组中大于30的元素 const score = [10, 50, 3, 40, 33] const re = score.filter(function (item) { return item > 30 }) console.log(re) // [50, 40, 33]
深入对象
创建对象的三种方式
- 利用对象字面量创建对象
const pig = { name: '八戒' }
- 利用 new Object 创建对象
// const obj = new Object() // obj.uname = '八戒' const obj = new Object({uname: '八戒'}) console.log(obj)
- 利用构造函数创建对象
构造函数
- 构造函数:是一种特殊的函数,主要用来初始化对象
- 使用场景:常规的{…} 语法允许创建一个对象。比如我们创建了佩奇的对象,继续创建乔治的对象还需要重新写一遍,此时可以通过构造函数来快速创建多个类似的对象。
- 构造函数在技术上是常规函数。不过有两个约定:
- 它们的命名以大写字母开头。
- 它们只能由"new"操作符来执行。
```JavaScript
function Pig(name, age, gender) {
this.name = name
this.age = age
this.gener = gender
}
// 创建八戒对象
const Bajie = new Pig('八戒',66666,'男')
// 创建天蓬元帅对象
const Tianpeng = new Pig('天蓬元帅',99999,'男')
// 创建八戒妈妈对象
const Mum = new Pig('裴氏夫人',23333,'女')
// 创建八戒爸爸对象
const Dad = new Pig('尉迟宗',114514,'男')
console.log(Bajie) // {name: '八戒', age: 66666, gener:'女'
```
-
构造函数语法:大写字母开头的函数
-
说明:
- 使用 new 关键字调用函数的行为被称为实例化
- 实例化构造函数时没有参数时可以省略()
- 构造函数内部无需写return,返回值即为新创建的对象
- 构造函数内部的return返回的值无效,所以不要写return
- new Object () new Date ()也是实例化构造函数
-
实例化执行过程
- 创建新空对象
- 构造函数this指向新对象
- 执行构造函数代码,修改this,添加新的属性
- 返回新对象
实例成员&静态成员
实例成员:通过构造函数创建的对象称为实例对象,实例对象中的属性和方法称为实例成员(实例属性和实例方法)
说明:
- 为构造函数传入参数,创建结构相同但值不同的对象
- 构造函数创建的实例对象彼此独立互不影响
静态成员:构造函数的属性和方法被称为静态成员(静态属性和静态方法)
说明:
- 静态成员只能构造函数来访问
- 静态方法中的this指向构造函数
- 比如:Date.now() Math.PI Math.random()
内置构造函数
在JavaScript中最主要的数据类型有6种:
- 基本数据类型:字符串、数值、布尔、undefined、null
- 引用类型:对象
- 但是,我们会发现有些特殊情况:
// 普通字符串 const str = 'andy' console.log(str.length) // 4
- 其实字符串、数值、布尔、等基本类型也都有专门的构造函数,这些我们称为包装类型。
- JS中几乎所有的数据都可以基于构成函数创建。
- 引用类型:Object, Array, RegExp, Date 等
- 包装类型:String, Number, Boolean 等
-
Object
- Object是内置的构造函数,用于创建普通对象
// 通过构造函数创建普通对象 const user = new Object({name: '小明', age: 15})
- 推荐使用字面量方式声明对象,而不是Object构造函数
- 学习三个常用静态方法(静态方法就是只有构造函数Object可以调用的)
- Object.keys
- 作用: Object.keys静态方法获取对象中所有属性(键)
- 语法:
const o = { name: '八戒', age: 114514 } // 获得对象的所有键,并且返回是一个数组 const arr = Object.keys(o) console.log(arr) // ['name', 'age']
- 注意: 返回的是一个数组
- Object.values
- 作用: Object.values静态方法获取对象中所有属性值
- 语法:
const o = { name: '八戒', age: 114514 } // 获得对象的所有值,并且返回是一个数组 const arr = Object.value(o) console.log(arr) // ['八戒', '114514']
- 注意: 返回的是一个数组
- Object. assign
- 作用: Object. assign静态方法常用于对象拷贝
- 语法:
// 拷贝对象 把 o 拷贝给 obj const o = { name: '八戒', age: 114514} const obj = {} Object.assign(obj, o) console.log(obj) // {name: '八戒', age: 114514} Object.assingn(o,{gender: '男'}) console.log(o) // {name: '八戒', age: 114514, gender: '男'}
- 使用:经常使用的场景给对象添加属性
-
Array
-
Array 是内置的构造函数,用于创建数组
const arr = new Array(3, 5) console.log(arr) // [3,5]
-
创建数组建议使用字面量创建,不用Array构造函数创建
-
数组常见实例方法-核心方法
方法 作用 说明 forEach 遍历数组 不返回数组,经常用于查找遍历数组元素 filter 过滤数组 返回新数组,返回的是筛选满足条件的数组元素 map 迭代数组 返回新数组,返回的是处理之后的数组元素,想要使用返回的新数组 reduce 累计器 返回累计处理的结果,经常用于求和等 - reduce
- 作用: reduce返回累计处理的结果,经常用于求和等基本
- 语法:
const arr = [1,2,3,4] arr.reduce(function(){}, 起始值) arr.reduce(function(上次值, 当前值){}, 起始值)
- 参数:如果有起始值,则把初始值累加到里面
- 执行过程:
- 如果没有起始值,则上一次值以数组的第一个数组元素的值
- 每一次循环,把返回值给做为 下一次循环的上一次值
- 如果有起始值,则 起始值做为上一次值
- reduce
-
数组常见方法-其他方法
- 实例方法 join 数组元素拼接为字符串,返回字符串(重点)
- 实例方法find查找元素,返回符合测试条件的第一个数组元素值,如果没有符合条件的则返回undefined(重点)
- 实例方法every检测数组所有元素是否都符合指定条件,如果所有元素都通过检测返回true,否则返回 false(重点)
- 实例方法 some检测数组中的元素是否满足指定条件,如果数组中有元素满足条件返回true,否则返回false
- 实例方法 concat 合并两个数组,返回生成新数组
- 实例方法 sort 对原数组单元值排序
- 实例方法 splice 删除或替换原数组单元
- 实例方法 reverse 反转数组
- 实例方法findIndex 查找元素的索引值
-
数组常见方法-伪数组转换为真数组
- 静态方法:Array.from()
-
-
String
- 在JavaScript 中的字符串、数值、布尔具有对象的使用特征,如具有属性和方法
// 字符串类型 const str = 'hello world!' // 统计字符的长度(字符数量) console.log(str.length) // 数值类型 const price = 12.345 // 保留两位小数 price.toFixed(2)
- 之所以具有对象特征的原因是字符串、数值、布尔类型数据是JavaScript底层使用Object构造函数"包装”来的,被称为包装类型。
- 常见实例方法
- 实例属性 length用来获取字符串的度长(重点)
- 实例方法 split(‘分隔符’)用来将字符串拆分成数组(重点)
- 实例方法 substring (需要截取的第一个字符的索引[,结束的索引号]) 用于字符串截取(重点)
- 实例方法 startswith(检测字符串[,检测位置索引号])检测是否以某字符开头(重点)
- 实例方法includes(搜索的字符串[,检测位置索引号])判断一个字符串是否包含在另一个字符串中,根据情况返回true 或 false(重点)
- 实例方法 toUpperCase 用于将字母转换成大写
- 实例方法 toLowerCase 用于将就转换成小写
- 实例方法 indexof 检测是否包含某字符
- 实例方法 endswith 检测是否以某字符结尾
- 实例方法 replace 用于替换字符串,支持正则匹配
- 实例方法 match 用于查找字符串,支持正则匹配
-
Number
- Number 是内置的构造函数,用于创建数值
- 常用方法:
- toFixed() 设置保留小数位的长度,四舍五入
- Number() 直接使用转数字
深入面向对象
编程思想
面向过程
- 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的依次调用就可以了。
- 面向过程,就是按照我们分析好了的步骤,按照步骤解决问题。
- 优点:性能比面向对象高,适合跟硬件联系很紧密的东西,例如单片机就采用的面向过程编程。
- 缺点:没有面向对象易维护、易复用、易扩展
面向对象
- 面向对象是把事务分解成为一个个对象,然后由对象之间分工与合作。
- 面向对象是以对象功能来划分问题,而不是步骤。
- 在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工。
- 面向对象编程具有灵活、代码可复用、容易维护和开发的优点,更适合多人合作的大型软件项目。
- 面向对象的特性:
- 封装性
- 继承性
- 多态性
- 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
- 缺点:性能比面向过程低
构造函数
- 封装是面向对象思想中比较重要的一部分, js面向对象可以通过构造函数实现的封装。
- 同样的将变量和函数组合到了一起并能通过this实现数据的共享,所不同的是借助构造函数创建出来的实例对象之间是彼此不影响的
- 前面我们学过的构造函数方法很好用,但是 存在浪费内存的问题
- 构造函数体现了面向对象的封装特性
- 构造函数实例创建的对象彼此独立、互不影响
原型
- 构造函数通过原型分配的函数是所有对象所共享的。
- JavaScript规定,每一个构造函数都有一个prototype属性,指向另一个对象,所以我们也称为原型对象
- 这个对象可以挂载函数,对象实例化不会多次创建原型上函数,节约内存
- 我们可以把那些不变的方法,直接定义在prototype对象上,这样所有对象的实例就可以共享这些方法。
- 构造函数和原型对象中的this 都指向实例化的对象
-
constructor属性
- 作用:该属性指向该原型对象的构造函数,简单理解,就是指向我的爸爸,我是有爸爸的孩子
- 使用场景:
- 如果有多个对象的方法,我们可以给原型对象采取对象形式赋值.
- 但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象constructor就不再指向当前构造函数了
- 此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。
-
对象原型
- **对象都会有一个属性__proto__**指向构造函数的prototype原型对象,之所以我们对象可以使用构造函数prototype原型对象的属性和方法,就是因为对象有__proto__原型的存在。
- 注意:
- proto 是JS非标准属性
- [[prototype]]和__proto__意义相同
- 用来表明当前实例对象指向哪个原型对象prototype
- __proto__对象原型里面也有一个constructor属性,指向创建该实例对象的构造函数
- 总结:
- prototype是什么?哪里来的?
- 原型(原型对象)
- 构造函数都自动有原型
- constructor属性在哪里?作用干啥的?
- prototype原型和对象原型_proto_里面都有
- 都指向创建实例对象/原型的构造函数
- proto_属性在哪里?指向谁?
- 在实例对象里面
- 指向原型 prototype
- prototype是什么?哪里来的?
-
原型继承
- 继承是面向对象编程的另一个特征,通过继承进一步提升代码封装的程度,JavaScript 中大多是借助原型对象实现继承的特性
- 龙生龙、凤生凤、老鼠的儿子会打洞描述的正是继承的含义。
-
原型链
- 基于原型对象的继承使得不同构造函数的原型对象关联在一起,并且这种关联的关系是一种链状的结构,我们将原型对象的链状结构关系称为原型链
- 查找规则
- 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
- 如果没有就查找它的原型(也就是__proto__指向的 prototype 原型对象)
- 如果还没有就查找原型对象的原型(Object的原型对象)
- 依此类推一直找到 Object 为止(null)
- __proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线
- 可以使用 instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
高阶技巧
深浅拷贝
首先浅拷贝和深拷贝只针对引用类型
-
浅拷贝:拷贝的是地址
- 常见方法:
- 拷贝对象: Object.assgin() /展开运算符{…obj)拷贝对象
- 拷贝数组: Array.prototype.concat() 或者[…arr]
- 直接赋值和浅拷贝有什么区别?
- 直接赋值的方法,只要是对象,都会相互影响,因为是直接拷贝对象栈里面的地址
- 浅拷贝如果是一层对象,不相互影响,如果出现多层对象拷贝还会相互影响
- 浅拷贝怎么理解?
- 拷贝对象之后,里面的属性值是简单数据类型直接拷贝值
- 如果属性值是引用数据类型则拷贝的是地址
- 常见方法:
-
深拷贝:拷贝的是对象,不是地址
- 常见方法:
- 通过递归实现深拷贝
- 函数递归:如果一个函数在内部可以调用其本身,那么这个函数就是递归函数
- 简单理解:函数内部自己调用自己,这个函数就是递归函数
- 递归函数的作用和循环效果类似
- 由于递归很容易发生"栈溢出”错误(stack overflow) ,所以必须要加退出条件return
- lodash/cloneDeep
- 通过JSON.stringify()实现
- 通过递归实现深拷贝
- 常见方法:
异常处理
了解 JavaScript 中程序异常处理的方法,提升代码运行的健壮性。
-
throw 抛出异常
function fn(x,y){ if(!x||!y){ // throw 'no param' throw new Error('no param') } return x+y } console.log(fn());
-
try/catch 捕获异常
-
我们可以通过try / catch 捕获错误信息(浏览器提供的错误信息) try 试试 catch 拦住 finally 最后
function fn(x,y){ try{ // 可能发生错误的代码 要写到try const p = document.querySelector('.p') p.style.color = 'red' }catch(err){ // 拦截浏览器提供的错误信息,但是不中断函数的执行 console.log(err.message) throw new Error('选择器错误') // 需要加 return 中断程序 return } finally{ //不管程序对不对,一定会执行的代码 alert('弹出对话框') } } console.log(fn());
-
try…catch 用于捕获错误信息
-
将预估可能发生错误的代码写在 try 代码段中
-
如果 try 代码段中出现错误后,会执行 catch 代码段,并截获到错误信息
-
finally 不管是否有错误,都会执行
-
-
debugger
- 放置到代码中,运行调试时可直接定位到debugger位置
处理this
- this 指向
- this是JavaScript最具“魅惑”的知识点A不同的应用场合this的取值可能会有意想不到的结果,在此我们对以往学习过的关于【this默认的取值】情况进行归纳和总结。
- 普通函数this指向
- 普通函数的调用方式决定了this的值,即【谁调用this的值指向谁】
- 普通函数没有明确调用者时this值为window,严格模式下没有调用者时this的值为undefined
- 箭头函数this指向
- 箭头函数中的 this 与普通函数完全不同,也不受调用方式的影响,事实上箭头函数中并不存在 this !
- 箭头函数会默认帮我们绑定外层 this 的值,所以在箭头函数中 this 的值和外层的 this 是一样的
- 箭头函数中的this引用的就是最近作用域中的this
- 向外层作用域中,一层一层查找this,直到有this的定义
- 注意
- 在开发中【使用箭头函数前需要考虑函数中this的值】 ,事件回调函数使用箭头函数时, this为全局的window因此DOM事件回调函数如果里面需要DOM对象的this,则不推荐使用箭头函数
- 同样由于箭头函数this的原因,基于原型的面向对象也不推荐采用箭头函数
- 改变 this
- JavaScript中还允许指定函数中this的指向,有3个方法可以动态指定普通函数中this的指向
- call()
- 使用 call 方法调用函数,同时指定被调用函数中 this 的值
- 语法:
fun.call(thisArg, arg1, arg2,...)
- thisArg:在 fun 函数运行时指定的 this 值
- arg1, arg2:传递的其他参数
- 返回值就是函数的返回值,因为它就是调用函数
- apply()
- 使用 apply方法调用函数,同时指定被调用函数中 this 的值
- 语法:
fun.apply(thisArg, [argsArray])
- thisArg:在fun函数运行时指定的 this 值
- argsArray:传递的值,必须包含在数组里面
- 返回值就是函数的返回值,因为它就是调用函数
- 因此 apply 主要跟数组有关系,比如使用 Math.max()求数组的最大值
- bind()
- bind()方法不会调用函数。但是能改变函数内部this 指向
- 语法:
fun.bind(thisAry,arg1,arg2,...)
- thisArg:在 fun 函数运行时指定的 this 值
- arg1, arg2: 传递的其他参数
- 返回由指定的 this 值和初始化参数改造的 原函数拷贝(新函数)
- 因此当我们只是想改变this指向,并且不想调用这个函数的时候,可以使用bind,比如改变定时器内部的this指向.
- 总结.
- 相同点:都可以改变函数内部的this指向.
- 区别点:
- call 和 apply 会调用函数,并且改变函数内部this指向.
- call 和 apply 传递的参数不一样, call 传递参数 aru1, aru2…形式 apply 必须数组形式[arg]
- bind 不会调用函数,可以改变函数内部this指向.
- 主要应用场景:
- call 调用函数并且可以传递参数
- apply 经常跟数组有关系.比如借助于数学对象实现数组最大值最小值
- bind 不调用函数,但是还想改变this指向.比如改变定时器内部的this指向.
性能优化
- 防抖-debounce
- 防抖:单位时间内,频繁触发事件,只执行最后一次
- 利用三方函数实现防抖
box.addEventListener('mousemove',_.debounce(mouseMove,500))
- 利用定时器(setTimeout)来实现
const box = document.querySelector('.box')
let i=1
function mouseMove(){
box.innerHTML = i++
// 如果存在开销较大的操作,可能造成卡顿
}
function debounce(fn,t){
let timer
return function(){
if(timer)clearTimeout(timer)
timer = setTimeout(function(){
fn()
},t)
}
}
box.addEventListener('mousemove',debounce(mouseMove,500))
- 节流-throttle
- 节流:单位时间内,频繁触发事件,只执行一次
- 使用场景:高频事件:鼠标移动mousemove、页面尺寸缩放resize、滚动条滚动scroll 等等
- 实现方法:
- lodash提供的节流函数来处理
- 手写一个节流函数
性能优化 | 说明 | 使用场景 |
---|---|---|
防抖 | 单位时间内,频繁触发事件,只执行最后一次 | 搜索框搜索输入、手机号、邮箱验证输入检测 |
节流 | 单位时间内,频繁触发事件,只执行一次 | 高频事件:鼠标移动 mousemove、页面尺寸缩放 resize、 滚动条滚动scroll 等等 |
HTML学习
Web移动端学习
JS基础学习
Web API学习
JS进阶学习
ajax学习
Node.js与Webpack学习
Git学习
vue学习—更新中
小程序学习—学习中
XML教学视频