javascript进阶——对之前的语法进行补充和优化

JavaScript 进阶 - 第1天

JS进阶 第一天

学习作用域、变量提升、闭包等语言特征,加深对 JavaScript 的理解,掌握变量赋值、函数声明的简洁语法,降低代码的冗余度。

  • 理解作用域对程序执行的影响
  • 能够分析程序执行的作用域范围
  • 理解闭包本质,利用闭包创建隔离作用域
  • 了解什么变量提升及函数提升
  • 掌握箭头函数、解析剩余参数等简洁语法

作用域

了解作用域对程序执行的影响及作用域链的查找机制,使用闭包函数创建隔离作用域避免全局变量污染。

作用域(scope)规定了变量能够被访问的“范围”,离开了这个“范围”变量便不能被访问,作用域分为全局作用域和局部作用域

局部作用域

局部作用域分为函数作用域块作用域

函数作用域

在函数内部声明的变量只能在函数内部被访问,外部无法直接访问。

<script>
// 声明 counter 函数
function counter(x, y) {
// 函数内部声明的变量
const s = x + y
console.log(s) // 18
}
// 设用 counter 函数
counter(10, 8)
// 访问变量 s
console.log(s)// 报错
</script>

总结:

  1. 函数内部声明的变量,在函数外部无法被访问
  2. 函数的参数也是函数内部的局部变量
  3. 不同函数内部声明的变量无法互相访问
  4. 函数执行完毕后,函数内部的变量实际被清空了

块作用域

在 JavaScript 中使用 {} 包裹的代码称为代码块,代码块内部声明的变量外部将【有可能】无法被访问。

<script>
{
// age 只能在该代码块中被访问
let age = 18;
console.log(age); // 正常
}

// 超出了 age 的作用域
console.log(age) // 报错

let flag = true;
if(flag) {
// str 只能在该代码块中被访问
let str = 'hello world!'
console.log(str); // 正常
}

// 超出了 age 的作用域
console.log(str); // 报错

for(let t = 1; t <= 6; t++) {
// t 只能在该代码块中被访问
console.log(t); // 正常
}

// 超出了 t 的作用域
console.log(t); // 报错
</script>

JavaScript 中除了变量外还有常量,常量与变量本质的区别是【常量必须要有值且不允许被重新赋值】,常量值为对象时其属性和方法允许重新赋值。

总结:

  1. let 声明的变量会产生块作用域,var 不会产生块作用域
  2. const 声明的常量也会产生块作用域
  3. 不同代码块之间的变量无法互相访问
  4. 推荐使用 letconst

注:开发中 letconst 经常不加区分的使用,如果担心某个值会不小被修改时,则只能使用 const 声明成常量。

全局作用域

<script> 标签和 .js 文件的【最外层】就是所谓的全局作用域,在此声明的变量在函数内部也可以被访问。

<script>
// 此处是全局

function sayHi() {
// 此处为局部
}

// 此处为全局
</script>

全局作用域中声明的变量,任何其它作用域都可以被访问,如下代码所示:

<script>
// 全局变量 name
const name = '小明'

// 函数作用域中访问全局
function sayHi() {
// 此处为局部
console.log('你好' + name)
}

// 全局变量 flag 和 x
const flag = true
let x = 10

// 块作用域中访问全局
if(flag) {
let y = 5
console.log(x + y) // x 是全局的
}
</script>

总结:

  1. window 对象动态添加的属性默认也是全局的,不推荐!window.i = 1
  2. 函数中未使用任何关键字声明的变量为全局变量,不推荐!!!
  3. 尽可能少的声明全局变量,防止全局变量被污染

JavaScript 中的作用域是程序被执行时的底层机制,了解这一机制有助于规范代码书写习惯,避免因作用域导致的语法错误。

作用域链

在解释什么是作用域链前先来看一段代码:

<script>
// 全局作用域
let a = 1
let b = 2
// 局部作用域
function f() {
let c
// 局部作用域
function g() {
let d = 'yo'
}
}
</script>

函数内部允许创建新的函数,f 函数内部创建的新函数 g,会产生新的函数作用域,由此可知作用域产生了嵌套的关系。

如下图所示,父子关系的作用域关联在一起形成了链状的结构,作用域链的名字也由此而来。

作用域链(可以说冒泡、就近)

作用域链本质上是底层的变量查找机制

  • 在函数被执行时,会优先查找当前函数作用域中查找变量,

  • 如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域,如下代码所示:

<script>
// 全局作用域
let a = 1
let b = 2

// 局部作用域
function f() {
let c
// let a = 10;
console.log(a) // 1 或 10
console.log(d) // 报错

// 局部作用域
function g() {
let d = 'yo'
// let b = 20;
console.log(b) // 2 或 20
}

// 调用 g 函数
g()
}

console.log(c) // 报错
console.log(d) // 报错

f();
</script>

总结:

  1. 嵌套关系的作用域串联起来形成了作用域链
  2. 相同作用域链中按着从小到大的规则查找变量
  3. 子作用域能够访问父作用域,父级作用域无法访问子级作用域

就像作用域像链子一样连接,从小到大的查找规则!

内存结构

B站视频:深入理解javascript内存管理0:22,参考文章:js内存分配机制

一、数据存储

简单数据类型:存储在栈里面,是操作系统帮我们管理内存空间,执行时,才会分配空间

引用数据类型:存储在堆里面,由程序员控制大小(管理内存空间)

二、变量生命周期

函数在执行时,简单数据存储在栈中,复杂类型存储在堆中,执行完就释放空间!

分三步走:分配空间 => 使用空间 => 释放空间

分配: 在js中,申明变量、函数、对象的时候,系统会⾃动为他们分配内存
使⽤: 使⽤变量、函数等,即读写内存
回收: 垃圾回收机制⾃动回收不再使⽤的内存

分配内存:

image-20230612164541421

根据系统位数,分配16M/32M内存,再拆成form空间和to空间,称为新生代,一开始读取都是放在form,把碎片化的空间整理后,再复制一份存放在to空间!

image-20230612172359548

历经多次释放后,生命周期较长的会单独分到老年态,像闭包分配的空间会跑到老年态里!

大于内存页的分配到大对象空间

热门函数,编译成机器代码,放到代码空间

每种内存都有垃圾回收机制!针对的是堆空间的内存!

垃圾回收机制(GC)

JS中内存的分配和回收,都是自动完成的,内存在不使用的时候,会被垃圾回收器自动回收!

内存的生命周期

JS环境中分配的内存,一般有如下生命周期(三个阶段):

  1. 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
  2. 内存使用:即读写内存,也就是使用变量、函数等
  3. 内存回收:使用完毕,由垃圾回收器自动回收不再使用的内存

说明:怎么判断使用完毕呢?

全局变量一般不会回收(除非关闭页面,全部都回收)

一般情况下局部变量的值,不用了,就会自动回收掉!就是读写完毕后!

内存泄漏

程序中分配的内存由于某种原因,程序未释放无法释放叫做内存泄漏!

JS垃圾回收机制-算法说明

堆栈空间分配区别:

  1. 栈(操作系统): 由操作系统自动分配释放函数的参数值、局部变量等,基本数据类型放到栈里面。
  2. 堆(操作系统): 一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收。复杂数据类型放到堆里面。

下面介绍两种常见的浏览器垃圾回收算法: 引用计数法 和 标记清除法

引用计数

不再使用的对象:就是看⼀个对象是否有指向它的引⽤, 如果没有其他对象指向它了,说明该对象已经不再需要了。
但如果两个对象相互引⽤(即循环引用),尽管他们已不再使⽤,垃圾回收不会进⾏回收,导致内存泄露

image-20230612181226388

image-20230612181349757

  1. 跟踪记录被引用的次数
  2. 如果被引用了一次,那么就记录次数1,多次引用会累加 ++
  3. 如果减少一个引用就减1 –
  4. 如果引用次数是0 ,则释放内存

image-20230612182249985

函数调用时,分配内存空间,那函数声明时,分配内存空间吗?

标记清除算法

标记清除算法将“不再使⽤的对象”定义为“⽆法达到的对象”:就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。
凡是能从根部到达的对象,都是还需要使⽤的。 那些⽆法由根部出发触及到的对象被标记为不再使⽤,稍后进⾏回收。

  1. 垃圾收集器在运⾏的时候会给存储在内存中的所有变量都加上标记。
  2. 从根部(global)出发将能触及到的对象的标记清除。
  3. 那些还存在标记的变量被视为准备删除的变量。
  4. 最后垃圾收集器会执⾏最后⼀步内存清除的⼯作,销毁那些带标记的值并回收它们所占⽤的内存空间。

内存泄漏

1、全局变量

function fn() {
text = ‘text content’; // 没有声明变量 实际上是全局变量 => window.text
this.text2 = ‘text2 content’ // 全局变量 => window.text2
}
fn();

2、未被清理的定时器、回调函数等

var resp = getRich();
setInterval(function() {
var app = document.getElementById(‘app’);
if(app) {
app.innerHTML = JSON.stringify(resp);
}
}, 5000); // 每 5 秒调⽤⼀次

注意:

如果后续 app 元素被移除,整个定时器实际上没有任何作⽤。
但如果没有回收定时器,整个定时器依然有效, 不但定时器⽆法被内存回收,定时器函数中的依赖也⽆法回收。
在上述案例中的 resp 也⽆法被回收。

3、闭包

在 JS 中,⼀个内部函数,有权访问包含其的外部函数中的变量(闭包)如下情况:闭包也会造成内存泄露


4、DOM引⽤

很多时候, 我们对 Dom 的操作, 会把 Dom node 的引⽤保存在⼀个数组或者 Map 中。

var elements = {
image: document.getElementById(‘imageId’)
};
function editPro() {
elements.image.src = ‘http://xxxxx.png’;
}
function removeEl() {
document.body.removeChild(document.getElementById(‘imageId’));
// 这个时候我们对于 #imageId 仍然有⼀个引⽤, Image 元素, 仍然⽆法被内存回收.
}

注意:即使我们通过操作 dom 把image元素给移除了,但是仍然有对 该image元素 的引用,仍然无法对其进行内存回收。

如何避免内存泄露
1、减少不必要的全局变量,可使⽤严格模式避免意外创建全局变量。
2、在使⽤完数据后,及时解除引⽤:赋值null(闭包中的变量,dom引⽤,定时器清除)。
3、组织好代码逻辑,避免死循环等造成浏览器卡顿,崩溃的问题。

基本类型所占Bytes:
number: 8字节
string: 每个长度 2 字节
boolean: 4 字节

闭包——是一种技巧

利用内存泄漏+作用域链,让外部访问函数内部私有变量,即内部变量可以不被回收,并外部函数访问

闭包是一种比较特殊和函数,使用闭包能够访问函数作用域中的变量。从代码形式上看闭包是一个做为返回值的函数,如下代码所示:

<body>
<script>
// 1. 闭包 : 内层函数 + 外层函数变量
// function outer() {
// const a = 1
// function f() {
// console.log(a)
// }
// f()
// }
// outer()

// 2. 闭包的应用: 实现数据的私有。统计函数的调用次数
// let count = 1
// function fn() {
// count++
// console.log(`函数被调用${count}次`)
// }

// 3. 闭包的写法 统计函数的调用次数
function outer() {
let count = 1
function fn() {
count++
console.log(`函数被调用${count}次`)
}
return fn
}
const re = outer()
// const re = function fn() {
// count++
// console.log(`函数被调用${count}次`)
// }
re()
re()
// const fn = function() { } 函数表达式
// 4. 闭包存在的问题: 可能会造成内存泄漏
</script>
</body>

总结:

1.怎么理解闭包?

  • 闭包 = 内层函数 + 外层函数的变量

注意:函数要调用才会被执行,才分配内存?

image-20230612190136133

2.闭包的作用?

  • 封闭数据,实现数据私有外部也可以访问函数内部的变量
  • 闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来
// 闭包的基本格式
function outer() {
let i = 1
function fn() {
console.log(i)
}
return fn
}
const fun = outer()
fun()
// 可以理解为,让另一个函数,成为了内部函数,按照作用域链,可以访问外层函数的变量!

需求:打印函数调用的次数

image-20230612194118411

function fn() {
let i = 0
return function fc() {
return i++
}
}
const fun = fn()
fun()
fun()
fun()
fun()
console.log(fun());

3.闭包可能引起的问题?

  • 内存泄漏

变量提升(了解var)

变量提升是 JavaScript 中比较“奇怪”的现象,它允许在变量声明之前即被访问,即当代码在执行之前,var声明的变量提升到当前作用域的最前面!只提升声明,不提升赋值!

<script>
// 访问变量 str
console.log(str + 'world!');

// 声明变量 str
var str = 'hello ';
</script>

总结:

  1. let、const变量在未声明即被访问时会报语法错误
  2. var变量在声明之前即被访问,变量的值为 undefined
  3. let 声明的变量不存在变量提升,推荐使用 let
  4. var变量提升出现在相同作用域当中,局部就在局部内前,全局下依旧不能用,会报错!
  5. 实际开发中推荐先声明再访问变量

注:关于变量提升的原理分析会涉及较为复杂的词法分析等知识,而开发中使用 let 可以轻松规避变量的提升,因此在此不做过多的探讨,有兴趣可查阅资料

函数

知道函数参数默认值、动态参数、剩余参数的使用细节,提升函数应用的灵活度,知道箭头函数的语法及与普通函数的差异。

函数提升

函数提升与变量提升比较类似,是指函数在声明之前即可被调用。即会把所有函数声明提升到当前作用域的最前面!只提升声明,当调用时随意写!

<script>
// 调用函数
foo()
// 声明函数
function foo() {
console.log('声明之前即被调用...')
}

// 不存在提升现象
bar() // 错误
var bar = function () {
console.log('函数表达式不存在提升现象...')
}
</script>

总结:

  1. 函数提升能够使函数的声明调用更灵活
  2. 函数表达式不存在提升的现象
  3. 函数提升出现在相同作用域当中

函数参数

函数参数的使用细节,能够提升函数应用的灵活度。除了形参和实参,形参可以赋予默认值,函数还有动态参数、剩余参数!

默认值

<script>
// 设置参数默认值
function sayHi(name="小明", age=18) {
document.write(`<p>大家好,我叫${name},我今年${age}岁了。</p>`);
}
// 调用函数
sayHi();
sayHi('小红');
sayHi('小刚', 21);
</script>

总结:

  1. 声明函数时为形参赋值即为参数的默认值
  2. 如果参数未自定义默认值时,参数的默认值为 undefined
  3. 调用函数时没有传入对应实参时,参数的默认值被当做实参传入

动态参数

arguments 是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参。仅存在于函数中

<script>
// 求生函数,计算所有参数的和
function sum() {
// console.log(arguments)
let s = 0
for(let i = 0; i < arguments.length; i++) {
s += arguments[i]
}
console.log(s)
}
// 调用求和函数
sum(5, 10)// 两个参数
sum(1, 2, 4) // 两个参数
</script>

总结:

  1. arguments 是一个伪数组,有索引和值,没有常用方法,仅几个个性化方法!
  2. arguments 的作用是动态获取函数的实参
  3. 可以使用for遍历,在该函数中获取传入的实参!

剩余参数(可用于函数和解构中)

剩余,就是传递的多余的实参,只能接受剩余的参数!(a,b,…other),按顺序传入实参,第二位的后面

<script>
function config(baseURL, ...other) {
console.log(baseURL) // 得到 'http://baidu.com'
console.log(other) // other 得到 ['get', 'json']
}
// 调用函数
config('http://baidu.com', 'get', 'json');
</script>

总结:

  1. ... 是语法符号,置于最末函数形参之前,用于获取多余的实参
  2. 借助 ... 获取的剩余实参,是个真数组

…arr与arguments相比,更加灵活!能使用数组的方法!提倡使用剩余参数!特别是箭头函数!

与展开运算符的区别(…)

剩余参数在函数中使用,访问时不加(…),仅在函数形参写成这样!展开运算符是任何地点都可以使用,并且使用时(…arr)!

  1. 展开运算符...,将一个数组进行展开
  2. 并不会修改原数组
  3. 最典型,求最大值、Math.max(…arr)。合并数组arr = [...arr1,...arrr2]

image-20230613165128746

箭头函数

箭头函数是一种声明函数的简洁语法,它与普通函数并无本质的区别,差异性更多体现在语法格式上。并且不绑定this!主要使用场景是需要匿名函数的地方!可以平替匿名函数!

普通函数更强调代码的复用,而箭头函数,多用于函数表达式,或者回调函数中!实现函数的简写!函数越简单,箭头函数越简洁,越好用!

image-20230613175944784

语法特点

语法1:基本写法() => {}

image-20230613171854783

语法2:只有一个参数可以省略小括号

语法3:如果函数体只有一行代码,可以写到一行上,并且无需写 return 直接返回值

语法4:加括号的函数体,返回对象字面量表达式

<body>
<script>
// const fn = function () {
// console.log(123)
// }
// 1. 箭头函数 基本语法
// const fn = () => {
// console.log(123)
// }
// fn()
// const fn = (x) => {
// console.log(x)
// }
// fn(1)
// 2. 只有一个形参的时候,可以省略小括号
// const fn = x => {
// console.log(x)
// }
// fn(1)
// // 3. 只有一行代码的时候,我们可以省略大括号
// const fn = x => console.log(x)
// fn(1)
// 4. 只有一行代码的时候,可以省略return
// const fn = x => x + x
// console.log(fn(1))
// 5. 箭头函数可以直接返回一个对象
// const fn = (uname) => ({ uname: uname })
// console.log(fn('刘德华'))

</script>
</body>

总结:

  1. 箭头函数属于表达式函数,因此不存在函数提升
  2. 箭头函数只有一个参数时可以省略圆括号 ()
  3. 箭头函数函数体只有一行代码时可以省略花括号 {},并自动做为返回值被返回

箭头函数参数

箭头函数中没有 arguments,只能使用 ... 动态获取实参

<body>
<script>
// 1. 利用箭头函数来求和
const getSum = (...arr) => {
let sum = 0
for (let i = 0; i < arr.length; i++) {
sum += arr[i]
}
return sum
}
const result = getSum(2, 3, 4)
console.log(result) // 9
</script>

箭头函数 this

箭头函数不会创建自己的this,它只会从自己的作用域链的上一层沿用this。

<script>
// 以前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()

// 2. 箭头函数的this 是上一层作用域的this 指向
// const fn = () => {
// console.log(this) // window
// }
// fn()
// 对象方法箭头函数 this
// const obj = {
// uname: 'pink老师',
// sayHi: () => {
// console.log(this) // this 指向谁? window
// }
// }
// obj.sayHi()

const obj = {
uname: 'pink老师',
sayHi: function () {
console.log(this) // obj
let i = 10
const count = () => {
console.log(this) // obj
}
count()
}
}
obj.sayHi()

</script>

对象中的箭头函数

箭头函数不会创建自己的this,它只会从自己的作用域链的上一层沿用this。即把这个this放到上一层去判断!在上一层新建一个this试试!

image-20230613192528953

事件中的箭头函数

在开发中【使用箭头函数前需要考虑函数中 this 的值】,事件回调函数使用箭头函数时,this 为全局的 window,因此 如果在DOM事件回调函数为了简便使用this,还是不太推荐使用箭头函数!

image-20230613193027474

解构赋值

知道解构的语法及分类,使用解构简洁语法快速为变量赋值。

解构赋值是一种快速为变量赋值的简洁语法,本质上仍然是为变量赋值,分为数组解构、对象解构两大类型。

数组解构

数组解构是将数组的单元值快速批量赋值给一系列变量的简洁语法

image-20230613195113324

如下代码所示:

<script>
// 普通的数组
let arr = [1, 2, 3]
// 批量声明变量 a b c
// 同时将数组单元值 1 2 3 依次赋值给变量 a b c
let [a, b, c] = arr
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
</script>

总结:[] = arr

  1. 赋值运算符 =的 左侧 [] ,用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量
  2. 变量的顺序对应数组单元值的位置依次进行赋值操作
  3. 变量的数量大于单元值数量时,多余的变量将被赋值为 undefined
  4. 变量的数量小于单元值数量时,可以通过 ... 获取剩余单元值,但只能置于最末位

image-20230613204017525

  1. 允许初始化变量的默认值,且只有单元值为 undefined 时默认值才会生效,类似参数传递!

image-20230613204207453

  1. 按需导入赋值

image-20230613204527339

注:支持多维解构赋值,比较复杂后续有应用需求时再进一步分析

image-20230613204921011

注意: js 前面必须加分号情况

数组作为开头时,编码器易解析成前面代码的一部分,加上结束的分号就能正常识别了!

image-20230613202823358

比如在交换值时:写在;[]

image-20230613202959526

总之:

image-20230613200711599

对象解构

对象解构是将对象属性和方法快速批量赋值给一系列变量的简洁语法,由于无序,根据属性名解构!

image-20230613220612587

如下代码所示:

<script>
// 普通对象
const user = {
name: '小明',
age: 18
};
// 批量声明变量 name age
// 同时将数组单元值 小明 18 依次赋值给变量 name age
const {name, age} = user

console.log(name) // 小明
console.log(age) // 18
</script>

总结:

  1. 赋值运算符 = 左侧的 {} 用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量
  2. 对象属性的值将被赋值给与属性名相同的变量
  3. 对象中找不到与变量名一致的属性时变量值为 undefined
  4. 允许初始化变量的默认值,属性不存在或单元值为 undefined 时默认值才会生效

注:支持多维解构赋值

对象解构的更改变量名

{:} = {},可以从一个对象中提取变量并同时修改新的变量名

const {name: username, age} = {name: '小明'age: 18}

冒号表示“什么值:赋值给谁”

数组对象解构:

image-20230613221712957

多级对象解构

image-20230613222307972

解构JSON对象

// 1. 这是后台传递过来的数据
const msg = {
"code": 200,
"msg": "获取新闻列表成功",
"data": [
{
"id": 1,
"title": "5G商用自己,三大运用商收入下降",
"count": 58
},
{
"id": 2,
"title": "国际媒体头条速览",
"count": 56
},
{
"id": 3,
"title": "乌克兰和俄罗斯持续冲突",
"count": 1669
},

]
}

// 需求1: 请将以上msg对象 采用对象解构的方式 只选出 data 方面后面使用渲染页面
// const { data } = msg
// console.log(data)

// 需求2: 上面msg是后台传递过来的数据,我们需要把data选出当做参数传递给 函数
// const { data } = msg
// msg 虽然很多属性,但是我们利用解构只要 data值,传递参数时,解构!
function render({ data }) {
// const { data } = arr
// 我们只要 data 数据
// 内部处理
console.log(data)

}
render(msg)

// 需求3, 为了防止msg里面的data名字混淆,要求渲染函数里面的数据名改为 myData
function render({ data: myData }) {
// 要求将 获取过来的 data数据 更名为 myData
// 内部处理
console.log(myData)

}
render(msg)

综合案例

forEach遍历数组

forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数

注意:

1.forEach 主要是遍历数组

2.参数当前数组元素是必须要写的, 索引号可选。

<body>
<script>
// forEach 就是遍历 加强版的for循环 适合于遍历数组对象
const arr = ['red', 'green', 'pink']
const result = arr.forEach(function (item, index) {
console.log(item) // 数组元素 red green pink
console.log(index) // 索引号
})
// console.log(result)
</script>
</body>

filter筛选数组

filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素

主要使用场景: 筛选数组符合条件的元素,并返回筛选之后元素的新数组

<body>
<script>
const arr = [10, 20, 30]
// const newArr = arr.filter(function (item, index) {
// // console.log(item)
// // console.log(index)
// return item >= 20
// })
// 返回的符合条件的新数组

const newArr = arr.filter(item => item >= 20)
console.log(newArr)
</script>
</body>

综合案例:筛选价格

// 1. 渲染函数
function render(arr) {
let str = ''
arr.forEach(item => {
let {name,price,picture} = item
str += `
<div class="item">
<img src="${picture}" alt="">
<p class="name">${name}</p>
<p class="price">${price}</p>
</div>
`
})
document.querySelector('.list').innerHTML = str
}


// 2. 给按钮绑定事件,点击筛选数组,并渲染
// document.querySelector('.filter').addEventListener('click', e => {
// // 筛选数组
// let over0 = []
// let over100 = []
// let over300 = []
// over0 = goodsList.filter(item => item.price > 0 && item.price < 100)
// over100 = goodsList.filter(item => item.price >= 100 && item.price < 300)
// over300 = goodsList.filter(item => item.price >= 300)
// // 渲染数组
// if (e.target.tagName === 'A') {
// if(e.target.dataset.index === '1'){
// render(over0)
// }else if(e.target.dataset.index === '2'){
// render(over100)
// }else if(e.target.dataset.index === '3'){
// render(over300)
// }else{
// render(goodsList)
// }
// }
// })
render(goodsList)

document.querySelector('.filter').addEventListener('click', e => {
// 当前元素对象的信息解构
const {tagName, dataset} = e.target
if (tagName === 'A') {
let arr = goodsList
if(dataset.index === '1'){
arr = goodsList.filter(item => item.price > 0 && item.price < 100)
}else if(dataset.index === '2'){
arr = goodsList.filter(item => item.price >= 100 && item.price < 300)
}else if(dataset.index === '3'){
arr = goodsList.filter(item => item.price >= 300)
}
render(arr)
}
})

小结:

  1. 箭头函数、解构、能让代码简洁,但功能思路还是差不多
  2. 知道全局变量占内存后,或许会写更多函数,或者代码块,来优化
  3. 这个关键简洁还是筛选数组的方法,不然的话,需要设置三个数组,根据价格筛选对象进行追加
  4. 解构对于大坨数据,仅需要其中一部分非常好用
  5. 箭头函数对于一句话返回真简洁
  6. 遍历对象数组追加字符串,对于渲染页面来说,真方便,除了有点长,但反正不用管内部细节!
  7. 但是需要CSS配合,不然的话,生成的HTML结构还是太骨感了!

总结:

  1. 学习了闭包,闭包形成的前提是作用域链和垃圾回收机制无法回收,也就是内存泄漏!
  2. 箭头函数,简洁性,与匿名函数的绝配(不需要this的匿名函数)
  3. (解构对于摘葡萄),对象中万千数据,只取其中几个!

JavaScript 进阶 - 第2天

JS进阶第二天

了解面向对象编程的基础概念及构造函数的作用,体会 JavaScript 一切皆对象的语言特征,掌握常见的对象属性和方法的使用。

  • 了解面向对象编程中的一般概念
  • 能够基于构造函数创建对象
  • 理解 JavaScript 中一切皆对象的语言特征
  • 理解引用对象类型值存储的的特征
  • 掌握包装类型对象常见方法的使用

深入对象

了解面向对象的基础概念,能够利用构造函数创建对象。

创建对象三种方式

  1. 利用对象字面量创建对象

  2. 利用 new Object 创建对象

image-20230615100011846

  1. 利用 new Object 创建对象

构造函数

构造函数

是一种特殊的函数,主要用来初始化对象,不要搞混构造函数和对象!构造函数是函数,需要先声明后调用

构造函数是专门用于创建对象的函数,如果一个函数使用 new 关键字调用,那么这个函数就是构造函数。

使用场景

常规的 {…} 语法允许创建一个对象。比如我们创建了佩奇的对象,继续创建乔治的对象还需要重新写一 遍,此时可以通过构造函数来快速创建多个类似的对象

image-20230615100829853

使用构成函数

不过有两个约定: 1. 它们的命名以大写字母开头。 2. 它们只能由 “new“ 操作符来执行

<script>
// 定义函数
function foo() {
console.log('通过 new 也能调用函数...');
}
// 调用函数
new foo;
</script>

总结:

  1. 使用 new 关键字调用函数的行为被称为实例化
  2. 实例化构造函数时没有参数时可以省略 ()
  3. 构造函数的返回值即为新创建的对象
  4. 构造函数内部的 return 返回的值无效!

注:实践中为了从视觉上区分构造函数和普通函数,习惯将构造函数的首字母大写。

实例化执行过程——new过程4步

为什么一个函数,可以生成对象,也就是实例化,这与普通函数的调用不同,通过new关键字,生成对象!

  1. 创建新的空对象
  2. 构造函数this指向新对象
  3. 执行构造函数代码,修改this,添加新的属性
  4. 返回新对象

image-20230615104036651

image-20230615104439259

注意:1和2是new实例化的特点,3是函数与对象的特点,4是函数的特点!

实例成员

通过构造函数创建的对象称为实例对象,实例对象中的属性(实例属性)和方法(实例属性)称为实例成员

<script>
// 构造函数
function Person() {
// 构造函数内部的 this 就是实例对象
// 实例对象中动态添加属性
this.name = '小明'
// 实例对象动态添加方法
this.sayHi = function () {
console.log('大家好~')
}
}
// 实例化,p1 是实例对象
// p1 实际就是 构造函数内部的 this
const p1 = new Person()
console.log(p1)
console.log(p1.name) // 访问实例属性
p1.sayHi() // 调用实例方法
</script>

总结:

  1. 构造函数内部 this 实际上就是实例对象,为其动态添加的属性和方法即为实例成员
  2. 为构造函数传入参数,动态创建结构相同但值不同的对象

注:构造函数创建的实例对象彼此独立互不影响。

静态成员

在 JavaScript 中底层函数本质上也是对象类型,因此允许直接为函数动态添加属性或方法,构造函数的属性和方法被称为静态成员。——函数可以添加属性和方法,数组可以添加属性和方法吗?属性和类似变量!

只能通过外部添加的方式,先声明一个函数,然后再为其添加属性和方法!毕竟是个函数,不能像对象一样,在内部直接添加属性,如果直接添加属性,这样是无法引用的!从作用域讲,外部无法访问函数内部值,并且函数无返回任何值,所以是不能添加在内部!只能在外部,这样可以当作对象!添加属性值!

<script>
// 构造函数
function Person(name, age) {
// 省略实例成员
}
// 静态属性
Person.eyes = 2
Person.arms = 2
// 静态方法
Person.walk = function () {
console.log('^_^人都会走路...')
// this 指向 Person
console.log(this.eyes)
}
</script>

总结:

  1. 静态成员指的是添加到构造函数本身的属性和方法,静态成员只能构造函数来访问!!!
  2. 一般公共特征的属性或方法静态成员设置为静态成员
  3. 静态成员方法中的 this 指向构造函数本身

注意:像之前使用Math.PI就是静态属性,Math.floor()就是静态方法,还有Date.now(),直接使用!

内置构造函数

掌握各引用类型和包装类型对象属性和方法的使用。

在 JavaScript 中最主要数据类型有 6 种:(保存在容器中的值!)

  1. 基本数据类型

分别是字符串、数值、布尔、undefined、null

  1. 引用类型

对象,常见的对象类型数据包括数组和普通对象。

基本包装类型

除了对象类型,有内置的属性和方法,甚至字符串数值简单数据类型也有属性和方法,说明并不简单,JS从底层对简单数据类型做了一层包装,它们都有专门的构造函数,用于创建对应类型的数据!

image-20230615114051204

非常好用的属性和方法——处理数据!

在 JavaScript 内置了一些构造函数,绝大部的数据处理都是基于这些构造函数实现的,JavaScript 基础阶段学习的 Date() 就是内置的构造函数。

<script>
// 实例化
let date = new Date();

// date 即为实例对象
console.log(date);
</script>

内置构造函数分类

引用类型:Object,Array,RegExp,Date 等

包装类型:String,Number,Boolean 等

Object

Object 是内置的构造函数,用于创建普通对象。推荐使用字面量!

<script>
// 通过构造函数创建普通对象
const user = new Object({name: '小明', age: 15})

// 这种方式声明的变量称为【字面量】
let student = {name: '杜子腾', age: 21}

// 对象语法简写
let name = '小红';
let people = {
// 相当于 name: name
name,
// 相当于 walk: function () {}
walk () {
console.log('人都要走路...');
}
}

console.log(student.constructor);
console.log(user.constructor);
console.log(student instanceof Object);
</script>

学下三个常用的静态方法:就是只能由构造函数Object调用的!

Object.keys()

获取对象的所有属性名(键),不需要for (let k in obj){},传入对象,以数组的形式返回对象的键!

Googs.vivo = '2299'
Googs.title = '充电5分钟等于5分钟充电'
console.log(Object.keys(Googs)); // [vivo,title]

Object.values()

传入对象,以数组的形式返回对象的属性值!

Object.assign(),拷贝对象,或者追加对象

image-20230615155822765

总结:

  1. 推荐使用字面量方式声明对象,而不是 Object 构造函数
  2. Object.assign() 静态方法创建新的对象
  3. Object.keys() 静态方法获取对象中所有属性
  4. Object.values() 表态方法获取对象中所有属性值

Array

Array 是内置的构造函数,用于创建数组。

<script>
// 构造函数创建数组
let arr = new Array(5, 7, 8);

// 字面量方式创建数组
let list = ['html', 'css', 'javascript']

</script>

数组赋值后,无论修改哪个变量另一个对象的数据值也会相当发生改变。

image-20230615160523565

image-20230615160719316

reduce(函数(上一次值, 当前值), 起始值)——求和

reduce() 返回函数累计处理的结果,经常用于求和等,起始值可以省略,如果写就作为第一次累计的起始值

image-20230615161141427

// 有初始值时,多加对应的值,没有初始值,就只累加数组内的值,记住用return返回!箭头一句省略
const total = arr.reduce((prev, current) => prev + current, 10)

累计值参数:
1. 如果有起始值,则以起始值为准开始累计, 累计值 = 起始值
2. 如果没有起始值, 则累计值以数组的第一个数组元素作为起始值开始累计
3. 后面每次遍历就会用后面的数组元素 累计到 累计值 里面 (类似求和里面的 sum )
  1. 如果没有起始值,则【上一次值】是,数组的第一个元素的值,如果有,则起始值作为【上一次值】
  2. 每次循环,把返回值作为下一次循环中【上一次值】,把数组中下一次元素当作【当前值】

没有初始值:

image-20230615162538175

有初始值:

image-20230615162900481

案例:算工资

arr = [{
uname: '张三'
salary: 10000
},{
uname: '李四'
salary: 10000
},{
uname: '王二'
salary: 10000
}]

分析,这是一个数组,但又不是单纯的数组!

方法一是把这个对象数组的salary全部拿出来,放到一个数组中

方法二更妙,依旧把这个当作数组,当遍历其中的对象时,加个点操作符,取对象中salary的值!

image-20230615163435355

注意:这里为了让遍历次数与元素(对象)个数一致,添加个0作为初始值!

数组:实例方法汇总——MDN手册

  1. 推荐使用字面量方式声明数组,而不是 Array 构造函数
  2. 实例方法 forEach 用于遍历数组,替代 for 循环 (重点)
  3. 实例方法 filter 过滤数组单元值,生成新数组(重点)
  4. 实例方法 map 迭代原数组,生成新数组(重点)
  5. 实例方法 join 数组元素拼接为字符串,返回字符串(重点)
  6. 实例方法 find 查找元素, 返回符合测试条件的第一个数组元素值,如果没有符合条件的则返回 undefined(重点)

image-20230615164909773

注意:对于普通函数来讲,这个方法的用处不大,只能通过元素查找并返回查找的元素!但是对于对象数组来讲,可以通过对象中某一个属性的值,来查找对象,并返回查找的对象,这就具有很大价值了!

  1. 实例方法every 检测数组所有元素是否都符合指定条件,如果所有元素都通过检测返回 true,否则返回 false(重点)——类似filter(),不过返回的是否满足,是布尔值!

  2. 实例方法some 检测数组中的元素是否满足指定条件 如果数组中有元素满足条件返回 true,否则返回 false

  3. 实例方法 concat 合并两个数组,返回生成新数组

  4. 实例方法 sort 对原数组单元值排序

  5. 实例方法 splice 删除或替换原数组单元

  6. 实例方法 reverse 反转数组

  7. 实例方法 findIndex 查找元素的索引值

    image-20230615164008400

案例:

image-20230615170951543

// 通过静态方法,获取对象的属性值,再使用join方法,拼接成字符串!
const str = Object.values(goodsList[1].spec).join('/')

静态方法

Array.from()——伪数组转化为真数组

包装类型

在 JavaScript 中的字符串、数值、布尔具有对象的使用特征,如具有属性和方法,如下代码举例:

<script>
// 字符串类型
const str = 'hello world!'
// 统计字符的长度(字符数量)
console.log(str.length)

// 数值类型
const price = 12.345
// 保留两位小数
price.toFixed(2) // 12.34
</script>

之所以具有对象特征的原因是字符串、数值、布尔类型数据是 JavaScript 底层使用 Object 构造函数“包装”来的,被称为包装类型。

String

String 是内置的构造函数,用于创建字符串。

<script>
// 使用构造函数创建字符串
let str = new String('hello world!');

// 字面量创建字符串
let str2 = '你好,世界!';

// 检测是否属于同一个构造函数
console.log(str.constructor === str2.constructor); // true
console.log(str instanceof String); // false
</script>

字符串:实例属性和方法汇总——MDN手册

  1. 实例属性 length 用来获取字符串的度长(重点)
  2. 实例方法 split('分隔符') 用来将字符串拆分成数组(重点)——与join()相反——一般通过字符来拆分!

image-20230615173239570

  1. 实例方法 substring(需要截取的第一个字符的索引[,结束的索引号]) 用于字符串截取(重点)——通过索引号来截取字符,但是注意:包前不包后!

  2. 实例方法 startsWith(检测字符串[, 检测位置索引号]) 检测是否以某字符开头(重点)——以字符串为实参

const str = 'pig是只🐖'
console.log(str.startsWith('pi')) // true

image-20230615185726739

  1. 实例方法 includes(搜索的字符串[, 检测位置索引号]) 判断一个字符串是否包含在另一个字符串中,根据情况返回 true 或 false(重点)——是否含有这个字符,无论位置

image-20230615190508745

  1. 实例方法 toUpperCase 用于将字母转换成大写
  2. 实例方法 toLowerCase 用于将就转换成小写
  3. 实例方法 indexOf 检测是否包含某字符
  4. 实例方法 endsWith 检测是否以某字符结尾
  5. 实例方法 replace 用于替换字符串,支持正则匹配
  6. 实例方法 match 用于查找字符串,支持正则匹配

注:String 也可以当做普通函数使用,这时它的作用是强制转换成字符串数据类型。

案例:清洗数据

image-20230615191342412

image-20230615192212987

等价于:image-20230615192526811

Number

Number 是内置的构造函数,用于创建数值。

<script>
// 使用构造函数创建数值
let x = new Number('10')
let y = new Number(5)

// 字面量创建数值
let z = 20

</script>

image-20230615193027932

总结:

  1. 推荐使用字面量方式声明数值,而不是 Number 构造函数
  2. 实例方法 toFixed 用于设置保留小数位的长度

案例:

// 1. 处理数据
document.querySelector('.list').innerHTML = goodsList.map(item => {
const {id, name, price, picture, count, spec, gift} = item
const text = Object.values(spec).join('/')
const subtotal = ((count*100*price)/100).toFixed(2)
const str = gift? gift.split(',').map(item => `<span class="tag">【赠品】${item}</span>`).join(''):''
return `
<div class="item">
<img src=${picture} alt="">
<p class="name">${name} ${str}</p>
<p class="spec">${text}</p>
<p class="price">${price.toFixed(2)}</p>
<p class="count">x${count}</p>
<p class="sub-total">${subtotal}</p>
</div>
`
}).join()

// 2. 总价
let total = goodsList.reduce((prev, current) => {
const {price, count} = current
return prev + price*100*count/100
}, 0).toFixed(2)
document.querySelector('.total .amount').innerHTML = total

小结:

  1. 思路是先渲染成页面,再替换部分数据(比起先处理数据后渲染,更连贯,不会出现二次)
  2. 处理数据,把数据转化为数组,便于遍历,拿到每一个值
  3. 当需要多次使用对象中的属性值时,使用解构
  4. 转化为数组后,遍历数组,既可以遍历又可以处理成的HTML字符,三步split().map().join(),处理字符串,处理好字符串后,又拼凑成字符串,用于修改内容!
  5. 注意,当对象中不一定有这个值时,需要先判断,后处理!三元运算符!
  6. 先变成好集中处理的数据,数组最合适,大部分数据通过遍历,依次处理
  7. 对象处理数据的优势是,汇聚不同类型的值,数组处理数据的优势是,集中同等类型的值
  8. 解决小数精度问题,先把小数转化为整数,计算后再转化为小数!

总结:

  1. 自制构造函数,使用自定义构造函数创建对象!构造函数的规则,构造函数的主要作用是初始化对象!
  2. 不要搞混构造函数与对象!如果是想创建带有特殊方法的对象,那么使用构造函数,如果想拥有解决问题的方法,那么使用对象!对象可以自带方法,构造函数不仅可以自带方法,还可以返回对象!
  3. 构造函数内部,写的属性根本不能使用!构造函数是个函数,虽然特殊,但毕竟是函数,需要调用,才能执行代码,一般构造函数是充满this,使用new执行时,this指向调用者,并添加值!从而生成对象!
  4. 直接使用构造函数的名字,返回个函数;直接调用构造函数,也没有任何作用;说明构造函数就是专门用于生产对象的!特别的,构造函数也可以像对象一样,通过点来添加属性!
  5. 本来只有对象,可以在内部添加属性和方法的,并且只有对象才能使用方法,但是在日常使用过程中,不免发现数组、字符串,甚至数字都有方法!这说明JS从底层上,把这些数据类型都处理成了对象!——内置构造函数,JS底层把基本数据类型,通过专门的构造函数变成包装类型!内置构造函数String/Number/Bloon
  6. 通过内置构造函数,主要讲了数据的处理,利用内置构造函数方法,来快速批量处理数据变成想要的格式!
  7. 其中处理数组最简洁,把数组处理成字符串,常见处理有遍历=>筛选、求和、映射(修饰)
  8. 其中数组中的方法,大多是遍历+回调函数,不断的调用函数来处理每一个元素!回调函数的参数要熟记
  9. 即,要不断地不断地对数组中的每一个元素进行处理,通过在方法中再传入回调函数,通过回调函数来遍历数组同时,处理每个元素,这种方法基本是数组的专属,其它数据类型很少有这类方法!
  10. 字符串不需要遍历,所以大部分都是查找方法,拼接、分割、截取、查找等方法

JavaScript 进阶 - 第3天笔记

JS进阶第三天

了解构造函数原型对象的语法特征,掌握 JavaScript 中面向对象编程的实现方式,基于面向对象编程思想实现 DOM 操作的封装。

  • 了解面向对象编程的一般特征
  • 掌握基于构造函数原型对象的逻辑封装
  • 掌握基于原型对象实现的继承
  • 理解什么原型链及其作用
  • 能够处理程序异常提升程序执行的健壮性

编程思想

学习 JavaScript 中基于原型的面向对象编程序的语法实现,理解面向对象编程的特征。——class—TS才用

面向过程

面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的依次调用就可以了。——解决问题的步骤(算法)

举个栗子:蛋炒饭

67679290689

面向对象

面向对象是把事务分解成为一个个对象,然后由对象之间分工与合作。—以对象功能来划分问题(拆分问题)

67679293032

在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工。

面向对象编程具有灵活、代码可复用、容易维护和开发的优点,更适合多人合作的大型软件项目。

面向对象的特性:

  • 封装性

  • 继承性

  • 多态性

构造函数

对比以下通过面向对象的构造函数实现的封装:

<script>
function Person() {
this.name = '佚名'
// 设置名字
this.setName = function (name) {
this.name = name
}
// 读取名字
this.getName = () => {
console.log(this.name)
}
}

// 实例对像,获得了构造函数中封装的所有逻辑
let p1 = new Person()
p1.setName('小明')
console.log(p1.name)

// 实例对象
let p2 = new Person()
console.log(p2.name)
</script>

封装是面向对象思想中比较重要的一部分,js面向对象可以通过构造函数实现的封装。

同样的将变量和函数组合到了一起并能通过 this 实现数据的共享,所不同的是借助构造函数创建出来的实例对象之间是彼此不影响的

总结:

  1. 构造函数体现了面向对象的封装特性
  2. 构造函数实例创建的对象彼此独立、互不影响

封装是面向对象思想中比较重要的一部分,js面向对象可以通过构造函数实现的封装。

前面我们学过的构造函数方法很好用,但是 存在浪费内存的问题——封装的函数,也被复制了多份!一样就行

原型对象——prototype——构造函数内

构造函数通过原型分配的函数是所有对象所共享的。——可以解决构造函数中,浪费内存的问题!

属于构造函数的属性,可以称为静态属性!每个构造函数都有,返回一个对象——称为(原型)原型对象!

  • JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象,所以我们也称为原型对象
  • 这个对象可以挂载函数,对象实例化不会多次创建原型上函数,节约内存
  • 我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。
  • 构造函数和原型对象中的this 都指向 实例化的对象

声明构造函数:

function Person() {

}
// 每个函数都有 prototype 属性
console.log(Person.prototype)
// 把公用的方法或者属性,可以直接挂在 prototype 对象上
Person.prototype.key = 1
Person.prototype.fn = function () {
console.log('666')
}

调用构造:了解了 JavaScript 中构造函数与原型对象的关系后,再来看原型对象具体的作用,如下代码所示:

<script>
function Person() {
// 此处未定义任何方法
}

// 为构造函数的原型对象添加方法
Person.prototype.sayHi = function () {
console.log('Hi~');
}

// 实例化
let p1 = new Person();
p1.sayHi(); // 输出结果为 Hi~
</script>

通过以上两个简单示例不难发现 JavaScript 中对象的工作机制:当访问对象的属性或方法时,先在当前实例对象是查找,然后再去原型对象查找,并且原型对象被所有实例共享。

总结:结合构造函数原型的特征,实际开发重往往会将封装的功能函数添加到原型对象中。

构造函数中this的指向——(重要)

构造函数和原型对象中的this都是指向 实例化的对象!首先声明在prototype 中的函数想要执行,得先调用,通过实例对象调用时,this指向调用者,也就是实例对象!

image-20230617160059138

案例:给数组添加方法、求最大值、求和

分析:给数组添加得方法,得放在内置构造函数Array的 prototype 里面!

// 传入数组的值呢? 通过this指向调用者本身,再用展开运算符+数学方法
Array.prototype.max = function () {
return Math.max(...this)
}
Array.prototype.sum = function () {
return this.reduce((prev, item) => prev + item, 0)
}
arr.max()
arr.sum()

constructor 属性——在prototype对象内

在哪里? 每个原型对象 prototype 里面都有个constructor 属性(constructor 构造函数)

作用:该属性指向该原型对象的构造函数, 简单理解,就是指向我的爸爸,我是有爸爸的孩子——构造函数

image-20230617163645857

console.log(Star.prototype.constructor === Star)		// 缔造者!
console.log(Array.prototype.constructor === Array) // true
console.log(String.prototype.constructor === String) // true

使用场景:

如果有多个对象的方法,我们可以给原型对象采取对象形式赋值.

但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了

此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。

Star.prototype = {
// 给原型对象赋值时,会覆盖constructor,可以再指向原构造函数!
constructor: Star
...
}

对象原型——__proto__

为什么实例对象能访问构造函数的prototype ,因为实例对象有__proto__

image-20230617172600323

对象都会有一个属性__proto__指向构造函数的 prototype 原型对象,于是可以使用构造函数 prototype

原型对象的属性和方法,就是因为对象有 __proto__ 原型的存在。

注意:

  • __proto__ 是JS非标准属性,取决于浏览器对这个意义的写法,Google写成[[prototype]]
  • [[prototype]]和__proto__意义相同,打印出[[prototype]],写代码用__proto__(只读,不可更改)!
  • 用来表明当前实例对象指向哪个原型对象prototype
  • __proto__对象原型里面也有一个 constructor属性,指向创建该实例对象的构造函数

实例对象的对象原型指向该构造函数的原型对象,原型对象和对象原型的constructor 属性又指向构造函数

构造函数和原型中的this,又指向实例对象!

image-20230617175400514

原型继承

继承是面向对象编程的另一个特征,通过继承进一步提升代码封装的程度,JavaScript 中大多是借助原型对象实现继承的特性。

龙生龙、凤生凤、老鼠的儿子会打洞描述的正是继承的含义。

<body>
<script>
// 继续抽取 公共的部分放到原型上
// const Person1 = {
// eyes: 2,
// head: 1
// }
// const Person2 = {
// eyes: 2,
// head: 1
// }
// 构造函数 new 出来的对象 结构一样,但是对象不一样
function Person() {
this.eyes = 2
this.head = 1
}
// console.log(new Person)
// 女人 构造函数 继承 想要 继承 Person
function Woman() {

}
// Woman 通过原型来继承 Person
// 父构造函数(父类) 子构造函数(子类)
// 子类的原型 = new 父类
Woman.prototype = new Person() // {eyes: 2, head: 1}
// 指回原来的构造函数
Woman.prototype.constructor = Woman

// 给女人添加一个方法 生孩子
Woman.prototype.baby = function () {
console.log('宝贝')
}
const red = new Woman()
console.log(red)
// console.log(Woman.prototype)
// 男人 构造函数 继承 想要 继承 Person
function Man() {

}
// 通过 原型继承 Person
Man.prototype = new Person()
Man.prototype.constructor = Man
const pink = new Man()
console.log(pink)
</script>
</body>

注意:在外面直接赋值,构造函数.prototype = {对象},紧接着就是构造函数.prototype.constructor = 构造函数

注意:对象赋值给多个构造函数时,由于对象是堆,所以赋值的是地址,地址里存放的是同一个对象值!通过容器名(地址)访问进行修改时,多个构造函数的值一同变化!要想不一样,再弄一个构造函数,new 构造函数就能生成不同的对象!

Woman.prototype = new Preson()
// 子类的原型 = new 父类

原型链

只要是对象就有对象原型__proto_,对象原型__proto_,指向构造函数的原型!——重复这两个操作!

基于原型对象的继承使得不同构造函数的原型对象关联在一起,并且这种关联的关系是一种链状的结构,我们将原型对象的链状结构关系称为原型链

67679338869

注意:构造函数的原型对象的对象原型,指向对象的原型对象!

<body>
<script>
// function Objetc() {}
console.log(Object.prototype)
console.log(Object.prototype.__proto__)

function Person() {

}
const ldh = new Person()
// console.log(ldh.__proto__ === Person.prototype)
// console.log(Person.prototype.__proto__ === Object.prototype)
console.log(ldh instanceof Person)
console.log(ldh instanceof Object)
console.log(ldh instanceof Array)
console.log([1, 2, 3] instanceof Array)
console.log(Array instanceof Object)
</script>
</body>

原型链——查找规则(为对象查找属性和方法提供一条路)

① 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。

② 如果没有就查找它的原型(也就是 __proto__指向的 prototype 原型对象)

③ 如果还没有就查找原型对象的原型(Object的原型对象)

④ 依此类推一直找到 Object 为止(null)

__proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线

⑥ 可以使用 instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

注意:一切数据皆对象,对象都有对象原型,对象原型指向构造函数的原型对象

  1. [] => Array.prototype, Array.prototype.__proto__ => Objct.prototype, Object.prototype => null
  2. '' => String.prototype, String.prototype.__proto__ => Object.prototype
  3. 对象实例的对象原型 => 构造函数的原型, 构造函数的原型的对象原型 => 对象的原型, 对象的原型指向空

意义:高级的构造函数的原型中存放的属性和方法,可以给子孙使用!只要一个原型中有方法,下面都能使用

console.log([1,2,3] instanceof Array)	// true
console.log(Array instanceof Object) // true
console.log([1,2,3] instanceof Object) // true

案例:面向对象编程模态框

微信图片_20230618140812

  1. 把这个模态框看成是一件独立的对象!由构造函数生成的实例对象,不同实例可以修改内部文字!
  2. 可以把构造函数中的this看成是一个实例对象,给这个对象添加属性和方法!
  3. 方法一般不写在构造函数体内,写到构造函数的原型对象中!
  4. 对象内拥有属性和方法,也就是用变量存储这个框的元素对象,所有的数据!DOM节点!
document.createElement('div')
  1. 然后用方法,把刚生成的对象添加到页面中,这里把它添加到body中,也就是在原型中添加open方法!
Modal.prototype.open = function () {
document.body.appendChild(元素对象)
}
  1. 然后再添加关闭方法,注意的是这个关闭方法,是与生成的模态框里的元素相关!在原型中添加方法的位置得注意,必须是模态框出现在页面中,才能绑定,于是放在open方法里面!可以把close方法独立再调用,我这里是直接把绑定点击和移除功能,写在了一起!
Modal.prototype.open = function () {
document.body.appendChild(this.box)

this.box.children[0].children[0].addEventListener('click', function () {
document.body.removeChild(document.querySelector('.modal'))
})
}

老师写法:

image-20230618134410684

完整代码:

// 设置生成对象的构造函数,也可以看成是一个即将生成的对象!
function Modal(tit = '', text = '') {
// 用一个变量存储一个元素对象,这是面向对象中——有什么属性
this.box = document.createElement('div')
this.box.className = 'modal'
this.box.innerHTML = `
<div class="header">${tit} <i>x</i></div>
<div class="body">${text}</div>
`
}
Modal.prototype.open = function () {
document.body.appendChild(this.box)
// 给小×绑定点击事件,这里通过元素关系找到×,再绑定通过父子关系移除刚生成的!
const modal = document.querySelector('.modal')
this.box.children[0].children[0].addEventListener('click', function () {
document.body.removeChild(modal)
})
}

// 先生成对象后,再去梳理对象之间得联系,这里是给删除元素对象绑定点击事件,来生成模态框!
document.querySelector('#delete').addEventListener('click', function () {
// 避免多次生成,如果存在就不生成
if(document.querySelector('.modal')){
return
}else {
// 生成一个显示删除的模态框对象!也就是心心念的对象!由刚才的构造函数生成
const del = new Modal('温馨提示','您没有删除权限操作')
del.open()
}
})

document.querySelector('#login').addEventListener('click', function () {
if(document.querySelector('.modal')){
return
}else {
// 生成对象,并马上调用展示这个对象的方法
const log = new Modal('友情提示','您还没有注册账号')
log.open()
}
})

老师的写法更优秀:

  1. 拆分各项功能,再考虑逻辑层面的,当什么时候,再做某事!
  2. 当点击移除时,更新当前模态框(我的是阻止更新,必须先关闭才能更新)
const mbox = document.querySelector('.modal')
mbox && mbox.remove()

总结:

  1. 原型给构造函数赋予了更多可能性,减少内存压力,子代能访问父代的方法!
  2. 把页面所有内容,看成一个个对象,这是DOM给出的各种属性和方法!
  3. 构造函数可以看成生产对象,或者就当个对象看!里面拥有属性和方法,属性是存储的数据,各种信息,构成了这个对象的血肉之躯,而方法是给对象赋予灵魂,让这个对象具有各种功能,最后再理清逻辑层面的关系,不同对象间的关系,功能间的依赖关系!
  4. 先生产一个个独立的对象,再把各个对象联系起来!通常是绑定事件,让其它对象具有控制该对象的方法!

JavaScript 进阶 - 第4天

JS进阶第四天

ajax和promise学了再学vue或者node,有空学学jquery

深浅拷贝

浅拷贝

首先浅拷贝和深拷贝只针对引用类型

浅拷贝:只能拷贝外一层值,内一层如果存在复杂数据,拷贝的是地址!拷贝浅浅的一层!

常见方法:

  1. 拷贝对象:Object.assgin() / 展开运算符 {…obj} 拷贝对象
  2. 拷贝数组:Array.prototype.concat() 或者 […arr]

注意:如果是简单数据类型拷贝值,引用数据类型拷贝的是地址 (简单理解: 如果是单层对象,没问题,如果有多层就有问题)

  • 直接赋值和浅拷贝有什么区别?

直接赋值的方法,只要是对象,都会相互影响,因为是直接拷贝对 象栈里面的地址  浅拷贝如果是一层对象,不相互影响,如果出现多层对象拷贝还会 相互影响

  • 浅拷贝怎么理解?

拷贝对象之后,里面的属性值是简单数据类型直接拷贝值  如果属性值是引用数据类型则拷贝的是地址

深拷贝

首先浅拷贝和深拷贝只针对引用类型

深拷贝:拷贝的是对象,不是地址

常见方法:

  1. 通过递归实现深拷贝(自己利用递归函数书写深拷贝)
  2. lodash/cloneDeep(利用JS库ladash里面的_.cloneDeep()
  3. 通过JSON.stringify()实现 (利用JSON字符串转换)

递归实现深拷贝

函数递归:

如果一个函数在内部可以调用其本身,那么这个函数就是递归函数

  • 简单理解:函数内部自己调用自己, 这个函数就是递归函数
  • 递归函数的作用和循环效果类似
  • 由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件 return
<body>
<script>
const obj = {
uname: 'pink',
age: 18,
hobby: ['乒乓球', '足球'],
family: {
baby: '小pink'
}
}
const o = {}
// 拷贝函数
function deepCopy(newObj, oldObj) {
debugger
for (let k in oldObj) {
// 处理数组的问题 一定先写数组 在写 对象 不能颠倒
if (oldObj[k] instanceof Array) {
newObj[k] = []
// newObj[k] 接收 [] hobby
// oldObj[k] ['乒乓球', '足球']
deepCopy(newObj[k], oldObj[k])
} else if (oldObj[k] instanceof Object) {
newObj[k] = {}
deepCopy(newObj[k], oldObj[k])
}
else {
// k 属性名 uname age oldObj[k] 属性值 18
// newObj[k] === o.uname 给新对象添加属性
newObj[k] = oldObj[k]
}
}
}
deepCopy(o, obj) // 函数调用 两个参数 o 新对象 obj 旧对象
console.log(o)
o.age = 20
o.hobby[0] = '篮球'
o.family.baby = '老pink'
console.log(obj)
console.log([1, 23] instanceof Object)
// 复习
// const obj = {
// uname: 'pink',
// age: 18,
// hobby: ['乒乓球', '足球']
// }
// function deepCopy({ }, oldObj) {
// // k 属性名 oldObj[k] 属性值
// for (let k in oldObj) {
// // 处理数组的问题 k 变量
// newObj[k] = oldObj[k]
// // o.uname = 'pink'
// // newObj.k = 'pink'
// }
// }
</script>
</body>

注意:判断为其它类型时,会递归函数,这个递归函数中,复杂类型剥层皮变成了简单类型,进行值拷贝!

先数组,进行递归,后对象进行递归!因为在instanceof方法中,数组也属于对象!

js库lodash里面cloneDeep内部实现了深拷贝

<body>
<!-- 先引用 -->
<script src="./lodash.min.js"></script>
<script>
const obj = {
uname: 'pink',
age: 18,
hobby: ['乒乓球', '足球'],
family: {
baby: '小pink'
}
}
const o = _.cloneDeep(obj)
console.log(o)
o.family.baby = '老pink'
console.log(obj)
</script>
</body>

JSON序列化——太简洁明了了!(换一个维度)

<body>
<script>
const obj = {
uname: 'pink',
age: 18,
hobby: ['乒乓球', '足球'],
family: {
baby: '小pink'
}
}
// 把对象转换为 JSON 字符串
// console.log(JSON.stringify(obj))
const o = JSON.parse(JSON.stringify(obj))
console.log(o)
o.family.baby = '123'
console.log(obj)
</script>
</body>

异常处理

了解 JavaScript 中程序异常处理的方法,提升代码运行的健壮性。

throw(抛异常)

异常处理是指预估代码执行过程中可能发生的错误,然后最大程度的避免错误的发生导致整个程序无法继续运行

总结:

  1. throw 抛出异常信息,程序也会终止执行
  2. throw 后面跟的是错误提示信息
  3. Error 对象配合 throw 使用,能够设置更详细的错误信息
<script>
function counter(x, y) {

if(!x || !y) {
// throw '参数不能为空!';
throw new Error('参数不能为空!')
}

return x + y
}

counter()
</script>

总结:

  1. throw 抛出异常信息,程序也会终止执行
  2. throw 后面跟的是错误提示信息
  3. Error 对象配合 throw 使用,能够设置更详细的错误信息

try … catch(捕获异常)

我们可以通过try / catch 捕获错误信息(浏览器提供的错误信息) try 试试 | catch 拦住 | finally 最后 |

<script>
function foo() {
try {
// 查找 DOM 节点
const p = document.querySelector('.p')
p.style.color = 'red'
} catch (error) {
// try 代码段中执行有错误时,会执行 catch 代码段,拦截错误,提示浏览器提供的错误信息
// 查看错误信息
console.log(error.message)
// 需要加return, 终止代码继续执行,或者抛个throw
return

}
finally {
// 不管上面程序对不对,一定会执行的代码
alert('执行')
}
console.log('如果出现错误,我的语句不会执行')
}
foo()
</script>

总结:

  1. try...catch 用于捕获错误信息
  2. 将预估可能发生错误的代码写在 try 代码段中
  3. 如果 try 代码段中出现错误后,会执行 catch 代码段,并截获到错误信息
  4. finally 不管是否有错误,都会执行

debugger

相当于断点调试,长代码里面,帮助打断点!

处理this

了解函数中 this 在不同场景下的默认值,知道动态指定函数 this 值的方法。

this 是 JavaScript 最具“魅惑”的知识点,不同的应用场合 this 的取值可能会有意想不到的结果,在此我们对以往学习过的关于【 this 默认的取值】情况进行归纳和总结。

环境对象this:指的是函数内部特殊的变量 this ,它代表着当前函数运行时所处的环境

普通函数

普通函数的调用方式决定了 this 的值,即【谁调用 this 的值指向谁】,如下代码所示:

<script>
// 普通函数
function sayHi() {
console.log(this)
}
// 函数表达式
const sayHello = function () {
console.log(this)
}
// 函数的调用方式决定了 this 的值
sayHi() // window
window.sayHi()


// 普通对象
const user = {
name: '小明',
walk: function () {
console.log(this)
}
}
// 动态为 user 添加方法
user.sayHi = sayHi
uesr.sayHello = sayHello
// 函数调用方式,决定了 this 的值
user.sayHi()
user.sayHello()
</script>

注: 普通函数没有明确调用者时 this 值为 window,严格模式下没有调用者时 this 的值为 undefined

开启严格模式也很简单:在需要设置的作用域前,写上'use strict'

箭头函数

箭头函数中的 this 与普通函数完全不同,也不受调用方式的影响,事实上箭头函数中并不存在 this !箭头函数中访问的 this 不过是箭头函数所在作用域的 this 变量。

  1. 箭头函数会默认帮我们绑定外层 this 的值,所以在箭头函数中 this 的值和外层的 this 是一样的
  2. 箭头函数中的this引用的就是最近作用域中的this
  3. 向外层作用域中,一层一层查找this,直到有this的定义
<script>

console.log(this) // 此处为 window
// 箭头函数
const sayHi = function() {
console.log(this) // 该箭头函数中的 this 为函数声明环境中 this 一致
}
// 普通对象
const user = {
name: '小明',
// 该箭头函数中的 this 为函数声明环境中 this 一致
walk: () => {
console.log(this)
},

sleep: function () {
let str = 'hello'
console.log(this)
let fn = () => {
console.log(str)
console.log(this) // 该箭头函数中的 this 与 sleep 中的 this 一致
}
// 调用箭头函数
fn();
}
}

// 动态添加方法
user.sayHi = sayHi

// 函数调用
user.sayHi()
user.sleep()
user.walk()
</script>

注意情况1:

在开发中【使用箭头函数前需要考虑函数中 this 的值】,事件回调函数使用箭头函数时,this 为全局的 window,因此DOM事件回调函数不推荐使用箭头函数,如下代码所示:

<script>
// DOM 节点
const btn = document.querySelector('.btn')
// 箭头函数 此时 this 指向了 window
btn.addEventListener('click', () => {
console.log(this)
})
// 普通函数 此时 this 指向了 DOM 对象
btn.addEventListener('click', function () {
console.log(this)
})
</script>

注意情况2:

同样由于箭头函数 this 的原因,基于原型的面向对象也不推荐采用箭头函数,如下代码所示:

<script>
function Person() {
}
// 原型对像上添加了箭头函数
Person.prototype.walk = () => {
console.log('人都要走路...')
console.log(this); // window
}
const p1 = new Person()
p1.walk()
</script>

总结:

  1. 函数内不存在this,沿用上一级的
  2. 不适用  构造函数,原型函数,dom事件函数等等
  3. 适用  需要使用上层this的地方
  4. 使用正确的话,它会在很多地方带来方便,后面我们会大量使用慢慢体会

改变this指向

以上归纳了普通函数和箭头函数中关于 this 默认值的情形,不仅如此 JavaScript 中还允许指定函数中 this 的指向,有 3 个方法可以动态指定普通函数中 this 的指向:

call()——了解

使用 call 方法调用函数,同时指定函数中 this 的值,使用方法如下代码所示:

<script>
// 普通函数
function sayHi() {
console.log(this);
}

let user = {
name: '小明',
age: 18
}

let student = {
name: '小红',
age: 16
}

// 调用函数并指定 this 的值
sayHi.call(user); // this 值为 user
sayHi.call(student); // this 值为 student

// 求和函数
function counter(x, y) {
return x + y;
}

// 调用 counter 函数,并传入参数
let result = counter.call(null, 5, 10);
console.log(result);
</script>

总结:

  1. call 方法能够在调用函数的同时指定 this 的值
  2. 使用 call 方法调用函数时,第1个参数为 this 指定的值
  3. call 方法的其余参数会依次自动传给函数做为函数的参数

apply()

使用 call 方法调用函数,同时指定函数中 this 的值,使用方法如下代码所示:

<script>
// 普通函数
function sayHi() {
console.log(this)
}

let user = {
name: '小明',
age: 18
}

let student = {
name: '小红',
age: 16
}

// 调用函数并指定 this 的值
sayHi.apply(user) // this 值为 user
sayHi.apply(student) // this 值为 student

// 求和函数
function counter(x, y) {
return x + y
}
// 调用 counter 函数,并传入参数
let result = counter.apply(null, [5, 10])
console.log(result)
</script>

总结:

  1. apply 方法能够在调用函数的同时指定 this 的值
  2. 使用 apply 方法调用函数时,第1个参数为 this 指定的值
  3. apply 方法第2个参数为数组,数组的单元值依次自动传入函数做为函数的参数,以数组方式传递实参
  4. 返回值 本身就是函数的返回值

可以用来求最大值Math.max.apply(Math, arr)

bind()

bind 方法并不会调用函数,而是创建一个指定了 this 值的新函数,使用方法如下代码所示:

<script>
// 普通函数
function sayHi() {
console.log(this)
}
let user = {
name: '小明',
age: 18
}
// 调用 bind 指定 this 的值
let sayHello = sayHi.bind(user);
// 调用使用 bind 创建的新函数
sayHello()
</script>

注:bind 方法创建新的函数,与原函数的唯一的变化是改变了 this 的值。用于不想执行,只想改变时!

返回由指定的this值和初始化参数改造的 原函数拷贝(新函数),但这个函数是更改过的!

例如:在定时器(延时函数)

// 需求,点击按钮,1秒后开启
btn.addEventListener('click', function () {
this.disabled = true
window.setTimenout(function () {
// 如果不改变指向,则这里的this指向window,通过bind(),可以指向btn
this.disabled = false
}bind(this), 1000) // 注意这里的this是外层与btn等价,也可以通过箭头函数跳出这层!
})
  • call 调用函数并且可以传递参数
  • apply 经常跟数组有关系. 比如借助于数学对象实现数组最大值最小值
  • bind 不调用函数,但是还想改变this指向. 比如改变定时器内部的this指向.

防抖节流

防抖(debounce)

所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间,例如回城B!可以用于输入框搜索,最后再请求,手机号或者邮箱验证!

image-20230619133615287

实现方式:

  1. lodash提供的防抖_.debounce(函数,时间)
  2. 手写一个防抖函数

思路:利用延时函数,如果有则清除延时函数,如果没有,则增添延时函数!延时函数内再调用要执行的函数!

案例:设置一个鼠标滑动时不添加,鼠标静止200ms后添加数字

分析:要执行增数函数,必须只有当前这一个延时函数,也就意味着鼠标滑动事件结束了200ms

let i = 0
let timer = null
function debounce() {
if(timer) clearTimeout(timer)
timer = setTimeout(function () {
box.innerHTML = i++
},200)
}
box.addEventListener('mousemove', debounce)

放在变量污染和扩展通用性,这里可以使用闭包和‘先执行再回调’,并把函数当作参数!

 const box = document.querySelector('.box')
// 增加数字
let i = 0
function addnum() {
box.innerHTML = ++i
}
// 自己封装的防抖函数,timer、f、t形成闭包
function debounce(f, t) {
let timer = null
return function fn(){
if(timer) clearTimeout(timer)
timer = setTimeout(function () {
f()
}, t)
}
}
// 给盒子绑定鼠标移动,// 注意这里的回调函数加小括号,先执行返回个函数,鼠标移动后,再执行内部的函数
box.addEventListener('mousemove', debounce(addnum, 200))

节流(throttle)

所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数

对于高频触发的事件:鼠标移动,滚动条滚动、页面缩放

image-20230619155453884

实现方式:

  1. lodash提供的防抖_.throttle(函数,时间)
  2. 手写一个节流函数

思路:利用定时器,每次执行前都先判断是否存在定时器,如果存在则不开启新定时器了,记得在定时器内部清空这个定时器

注意:开启的定时器,从内部是不能用clearTimeout(timer)进行清空的!但可以用赋值null

// 增加数字
let i = 0
function addnum() {
box.innerHTML = ++i
}
// 自己封装的防抖函数,timer、f、t形成闭包
function throttle(f, t) {
let timer = null
return function fn(){
if(!timer) {
timer = setTimeout(function () {
f()
timer = null
}, t)
}
}
}
// 给盒子绑定鼠标移动
box.addEventListener('mousemove', throttle(addnum, 500))

image-20230619162820761

节流综合案例

页面打开,可以记录上一次的视频播放位置

分析: 两个事件:

①:ontimeupdate 事件在视频/音频(audio/video)当前的播放位置发送改变时触发

②:onloadeddata 事件在当前帧的数据加载完成且还没有足够的数据播放视频/音频(audio/video)的 下一帧时触发——简而言之就是页面打开时触发!

谁需要节流? ontimeupdate事件, 触发频次太高了,我们可以设定 1秒钟触发一次!

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
<script>
// 1 获取元素, 对视频进行操作
const video = document.querySelector('video')
// 1.2 使用lodash的节流函数
video.ontimeupdate = _.throttle(() => {
// 1.3 把值存入到本地存储
localStorage.setItem('currentTime', video.currentTime)
},1000)

// 2 下次打开时从上次播放结束处开始
video.onloadeddata = () => {
video.currentTime = localStorage.getItem('currentTime') || 0
}
</script>

小结:这些知识可以面试前再复习一遍,使用场景不多,先抓住DOM操作!

  1. 深拷贝,使用JSON字符串转化,轻轻松松
  2. 处理bug,调试后,再try,然后catch,catch中再抛出错误,最后再finadly
  3. 环境对象this,是给函数找妈,记住常用的就行
  4. 改变this指向,call()与apply()是一对,bind()返回的是函数,常用在改变定时器!
  5. 防抖是一种算法:记住步骤,多写几遍,先可以不写成闭包形式,再整理成闭包形式!
  6. 节流挺常见:比防抖简单,注意关闭定时器用赋值空!
  7. 对于on事件,使用自制函数,记得调用!使用lodash库,记得先引入库文件!