JavaScript入门-JS进阶
作用域
局部作用域
- 函数作用域
- 块作用域 如 for 内
全局作用域
<script>标签 和.js文件 的【最外层】就是所谓的全局作用域,在此声明的变量在函数内部也可以被访问。全局作用域中声明的变量,任何其它作用域都可以被访问
对于var变量,其声明后就是全局变量
- var变量有变量提升的说法:
- 如果使用了 还未 声明的变量,编译器会把 后面声明的变量的“声明”(注意只有声明,不会将赋值语句提升)提升到作用域的最前面
就近原则
JS垃圾回收机制
JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收。
内存的生命周期
JS环境中分配的内存, 一般有如下生命周期:
- 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
- 内存使用:即读写内存,也就是使用变量、函数等
- 内存回收:使用完毕,由垃圾回收自动回收不再使用的内存
- 说明:
- 全局变量一般不会回收(关闭页面回收);
- 一般情况下局部变量的值, 不用了, 会被自动回收掉
堆栈空间分配区别:
- 栈(操作系统): 由操作系统自动分配释放函数的参数值、局部变量等,基本数据类型放到栈里面。
- 堆(操作系统): 一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收。复杂数据类型放到堆里面。
垃圾回收算法
下面介绍两种常见的浏览器垃圾回收算法: 引用计数法(IE浏览器) 和 标记清除法(其实就是可达性分析法)
引用计数
IE采用的引用计数算法, 定义“内存不再使用”,就是看一个对象是否有指向它的引用,没有引用了就回收对象
算法:
- 跟踪记录被引用的次数
- 如果被引用了一次,那么就记录次数1,多次引用会累加 ++
- 如果减少一个引用就减1 —
- 如果引用次数是0 ,则释放内存
存在一个致命的问题:嵌套引用(循环引用)
如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露。
函数进阶
l
标记清除法(可达性分析法)
核心:从根部扫描对象,能查找到的就是使用的,查找不到的就要回收
- 标记清除算法将“不再使用的对象”定义为“无法达到的对象”。
- 就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。 凡是能从根部到达的对象,都是还需要使用的。
- 那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
闭包
一个函数对周围状态的引用捆绑在一起,内层函数中访问到其外层函数的作用域
简单理解:闭包 = 内层函数 + 外层函数的变量 (简单来说就是有一个函数套了一层函数,内部的函数调用了外部函数的局部变量)
闭包作用:封闭数据,提供操作,外部也可以访问函数内部的变量
function outer(){
let i=0
function fn(){
i++
console.log(i)
}
return fn
}
const fun = outer()
fun()// 1 相当函数外部访问了函数内部 的局部变量
这样其实简单实现了一个 统计 函数调用次数的 函数,每次通过fun调用,不会使 i 置零,有点类似 java中new了一个实例变量(里面的成员变量是私有的,外部不能直接访问)
函数进阶
函数提升
函数生命前就可以调用
编译器会把所有函数的声明放到当前作用域的最前面
但是函数表达式不能提升(函数表达式相当于变量的赋值,因为之提升了声明所以不能使用)
函数的参数
函数默认参数: function fun(x=10)
函数动态参数
- 参数数量不定
function sum(){
let s = 0
for(let i=0;i<arguments.length;i++){
s+=arguments[i];
}
return s;
}
console.log(sum(1,2,3,4))
arguments 是一个伪数组,只存在于函数中
函数剩余参数(展开运算符)
剩余参数允许我们将一个不定数量的参数表示为一个数组
- … 是语法符号,置于最末函数形参之前,用于获取多余的实参
- 借助 … 获取的剩余实参,是个真数组
function config(baseURL,..other){
console.log(baseURL)
console.log(other)// 接受成一个真数组
}
config('baidu.com','get','json')
单独使用…作为展开运算符使用,可以将数组展开
求数组最大最小值
Math.max(...nums)
合并数组
const arr3 = [...arr1,...arr2]
箭头函数
基本语法
const fn = function (){
console.log(123)
}
// 1、箭头函数
const fn = () => {
console.log(123)
}
//
const fn = (x,y)=>{
函数体
}
// 调用
fn(x,y)
// 2、只有一个形参时可以省略小括号,(有多个参数或者没有参数都不能省略)
const fn = x =>{
函数体
}
fn(x)
// 3、只有一行代码时可以省略大括号,
const fn = x => console.log(x)
// 4、如果这一行就是返回语句,可以省略return
const fn = x => x+x
// 5、箭头函数可以返回一个对象
const fn = (username) => ({name: username })
举例
阻止表单的默认提交事件
const form = document.querySelector('form')
form.addEventListener('click',ev=>ev.preventDefault())
箭头函数的this
普通的this 是 谁调用,this 就指向谁
重要:!!!!!箭头函数不会创建自己的this,只会从自己的作用域链的上一层沿用this(但是普通函数会创建自己的this)
如
<script>
const fn = () => {
console.log(this)
}
</script>
这里的this ,指的其实是windows(script 域 的this指的就是 windows)
<script>
const obj = {
uname: "Mr.D"
sayHi: ()=>{
console.log(this) // this 指向谁?
}
}
obj.sayHi() // window
</script>
<script>
const obj = {
uname: "Mr.D"
sayHi: function(){
console.log(this) // this 指向谁?
let i =1
const sayHello = () =>{
console.log(this) // this 指向谁?
}
}
}
答:
obj.sayHi() // obj 里面的sayHello 也是 obj
</script>
解构和赋值
数组结构
数组的解构:把数组元素快速批量赋值给变量
const [max,min,avg] = [100,60,80]
// 交换变量
let a=1
let b=2; 必须加分号
[b,a]=[a,b]
PS 必须加分号的情况
- 两个立即执行函数之间用分号间隔
- 数组结构之前的语句
对象结构
const obj = {
uname:"taotaozi"
age:18
}
// 解构
const {uname,age}={ uname:"taotaozi", age:18}
// 要求 结构的变量名与属性名相同。。。为了解决这种情况,结构的变量名可以改名 旧变量名: 新变量名
const {uname:username,age}={ uname:"taotaozi", age:18}
解构对象数组
const pig = [{
uname:"佩奇"
age:17
}]
const [{uname,age}]=pig
常见解析案例
const response = {
"code":200,
"msg":"获取对象元素列表成功",
"data":[
{
"id":1,
"name":"ttz"
},
{
"id":2,
"name":"ddz"
}
]
}
// 获取数据部分
// const {data}= response // 可省略
//渲染
render({data}){ //传参同时解构 data是数组
}
// 为了避免冲突也可以改名
render({data:myData}){
for(dataitem:myData){
}
// or
myData.forEach(function(item,index){
console.log(item)
console.log(index)
})
// or 因为 item必须 index 可选
myData.forEach(item=>{
console.log(item)
})
}
内置对象/包装数据类型
包装类的隐式转换
在 JavaScript 中最主要的数据类型有 6 种:
基本数据类型:
字符串、数值、布尔、undefined、null
引用类型:
对象
如 字符串 按理说 因为他不是对象,所以应该是没有 长度(length)等属性,但是却有;如 num.toFixed(2)
其实字符串、数值、布尔、等基本类型也都有专门的构造函数,这些我们称为包装类型
其实其内部可以这样理解:
cosnt str = 'pink'
// 底层应该经过了这样的转化
const str = new String('pink') // 底层自动帮我们转换的
console.log(str.length) //调用的是引用数据类型的属性和方法
内置对象
Object
- Object 是内置的构造函数,用于创建普通对象。
- Object.keys 静态方法获取对象中所有属性(键)
- Object.values 静态方法获取对象中所有属性值
- Object. assign 静态方法常用于对象拷贝//尝试用添加属性
const o = { name : 'peiqi',age :6}
// 普通遍历方式
for(let k in o){
k o[k]
}
// Object.keys 静态方法获取对象中所有属性(键),返回一个数组
const keys = Object.keys(o) // ['name','age']
const values = Object.values(o) // ['peiqi',6]
// 拷贝对象 从 o 到 o1
const o1={}
Object.assign(o1,o)
Array
const arr = Array(3,5)
const arr = [3,5]
数组赋值后,无论修改哪个变量另一个对象的数据值也会相当发生改变。
总结:
推荐使用字面量方式声明数组,而不是
Array构造函数实例方法
forEach用于遍历数组,替代for循环 (重点)实例方法
filter过滤数组单元值,生成新数组(重点)实例方法
map迭代原数组,生成新数组(重点)实例方法
join数组元素拼接为字符串,返回字符串(重点)实例方法
find查找元素, 返回符合测试条件的第一个数组元素值,如果没有符合条件的则返回 undefined(重点)实例方法
every检测数组所有元素是否都符合指定条件,如果所有元素都通过检测返回 true,否则返回 false(重点)实例方法
some检测数组中的元素是否满足指定条件 如果数组中有元素满足条件返回 true,否则返回 false实例方法
concat合并两个数组,返回生成新数组实例方法
sort对原数组单元值排序实例方法
splice删除或替换原数组单元实例方法
reverse反转数组实例方法
findIndex查找元素的索引值
reduce
arr.reduce(function(){return?},起始值)
arr.reduce(fucntion(上一次值,当前值){ return ?},起始值)
起始值可以省略,那么起始值就是数组第一项,当前值是第二项
举例
const arr = [1,3,5]
const count = arr.reduce((prec,item)=>prev+item)
console.log(count)
其他 从 mdn 文档查找吧
包装数据类型
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>
总结:
- 实例属性
length用来获取字符串的度长(重点) - 实例方法
split('分隔符')用来将字符串拆分成数组(重点) - 实例方法
substring(需要截取的第一个字符的索引[,结束的索引号])用于字符串截取(重点) - 实例方法
startsWith(检测字符串[, 检测位置索引号])检测是否以某字符开头(重点) - 实例方法
includes(搜索的字符串[, 检测位置索引号])判断一个字符串是否包含在另一个字符串中,根据情况返回 true 或 false(重点) - 实例方法
toUpperCase用于将字母转换成大写 - 实例方法
toLowerCase用于将就转换成小写 - 实例方法
indexOf检测是否包含某字符 - 实例方法
endsWith检测是否以某字符结尾 - 实例方法
replace用于替换字符串,支持正则匹配 - 实例方法
match用于查找字符串,支持正则匹配
注:String 也可以当做普通函数使用,这时它的作用是强制转换成字符串数据类型。
Number
Number 是内置的构造函数,用于创建数值。
<script>
// 使用构造函数创建数值
let x = new Number('10')
let y = new Number(5)
// 字面量创建数值
let z = 20
</script>
总结:
- 推荐使用字面量方式声明数值,而不是
Number构造函数 - 实例方法
toFixed用于设置保留小数位的长度
深浅拷贝
浅拷贝
如果直接 用 直接赋值 作为复制对象的方法 , 其实 复制的是堆里面对象的地址
常见方法:
- 拷贝对象: Object.assign() // 展开运算符 {…obj} 拷贝对象
- 拷贝数组: Array.prototype.concat() 或者 […arr]
直接赋值和浅拷贝有什么区别?
直接赋值的方法,只要是对象,都会相互影响,因为是直接拷贝对象栈里面的地址
浅拷贝如果是一层对象(对象里面全是基本属性,没有对象套对象),不相互影响,如果出现多层对象拷贝还会相互影响
浅拷贝怎么理解?
拷贝对象之后,里面的属性值是简单数据类型直接拷贝值
如果属性值是引用数据类型则拷贝的是地址
深拷贝
递归实现深拷贝
函数递归:
如果一个函数在内部可以调用其本身,那么这个函数就是递归函数
- 简单理解:函数内部自己调用自己, 这个函数就是递归函数
- 递归函数的作用和循环效果类似
- 由于递归很容易发生“栈溢出”错误(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>
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>
异常处理
抛出
总结:
- throw 抛出异常信息,程序也会终止执行
- throw 后面跟的是错误提示信息
- Error 对象配合 throw 使用,能够设置更详细的错误信息
<script>
function counter(x, y) {
if(!x || !y) {
// throw '参数不能为空!';
throw new Error('参数不能为空!')
}
return x + y
}
counter()
</script>
捕获异常
总结:
try...catch用于捕获错误信息- 将预估可能发生错误的代码写在
try代码段中 - 如果
try代码段中出现错误后,会执行catch代码段,并截获到错误信息
try … catch
<script>
function foo() {
try {
// 查找 DOM 节点
const p = document.querySelector('.p')
p.style.color = 'red'
} catch (error) {
// try 代码段中执行有错误时,会执行 catch 代码段
// 查看错误信息
console.log(error.message)
// 终止代码继续执行
return
}
finally {
alert('执行')
}
console.log('如果出现错误,我的语句不会执行')
}
foo()
</script>
debugger
相当于断点调试
处理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
user.sayHello()
</script>
注: 普通函数没有明确调用者时 this 值为 window,严格模式下没有调用者时 this 的值为 undefined。
箭头函数
箭头函数中的 this 与普通函数完全不同,也不受调用方式的影响,事实上箭头函数中并不存在 this !箭头函数中访问的 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>
简单来说就是一层一层往外找this 直到找到 有定义的this
同样由于箭头函数 this 的原因,基于原型的面向对象也不推荐采用箭头函数,如下代码所示:
<script>
function Person() {
}
// 原型对像上添加了箭头函数
Person.prototype.walk = () => {
console.log('人都要走路...')
console.log(this); // window
}
const p1 = new Person()
p1.walk()
</script>
改变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>
总结:
call方法能够在调用函数的同时指定this的值- 使用
call方法调用函数时,第1个参数为this指定的值 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>
总结:
apply方法能够在调用函数的同时指定this的值- 使用
apply方法调用函数时,第1个参数为this指定的值 apply方法第2个参数为数组,数组的单元值依次自动传入函数做为函数的参数
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 的值。
防抖节流
- 防抖(debounce)
所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间 - 节流(throttle)
所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数




