Day1—浏览器存储

本地存储

概念

正常情况下,两个页面之间的数据是无法进行沟通的,那我们如何在一个页面登录后另一个页面能够获取到我们的登录状态呢,是因为浏览器提供了一个存储空间

在浏览器页面,打开控制台可以看到应用application选项,在他下面有一个本地存储localStroage,里面就是用来存储需要传递给别的网页的信息

localStroage是按照域名通讯的,不能跨浏览器交互,是永久性存储,只要不清除浏览器数据就一直存在,这也是为什么关闭浏览器后再打开仍然保留登录信息的原因

存储大小为5M,只能存储字符串,因此数据必须转成字符串的格式,这就需要上文提到的序列化转成JOSN串

操作

localStorage.setItem('关键字', '数据'),设置内容,也就是存储

如果是对象或者数组内容,则要转成JOSN串localStorage.setItem('关键字', JSON.stringify({ name: 'Jack' }))

localStorage.getItem('关键字'),获取内容,如果是数组或者对象需要反序列化

localStorage.removeItem('关键字'),移除内容,一次移除一个

localStorage.clear(),清空所有内容

会话存储

概念

同样的在应用application选项下有一个会话存储sesstionStorage

他和本地存储很像,不同点在于生命周期是会话级,也就是浏览器关闭后存储的内容就会消失

操作

操作也和localStorage很像

sessionStorage.setItem('关键字', '数据'),设置内容

sessionStorage.getItem('关键字'),获取内容

sessionStorage.removeItem('关键字'),移除一个内容

sessionStorage.clear(),清空所有


Day2—面向对象编程

概念

面向对象编程OOP是一种思想,并非一种语言,他是由面向过程、面向函数更进一步得来的

而在了解面向对象前我们需要了解类和对象的概念

类:一组对象的统一集合,他统一规定了一类对象的共同描述,也就是模版,比如人这个概念就是一个类,每个人的共同点就构成了人类,他内部包含了属性和方法

对象:是类的实例化,比如你、我就是人类这个类的具体对象,他应该具有一组属性以及对这些属性的操作方法,客观世界是由对象和对象之间的联系组成的

面向对象一般指的是基于类的面向对象,他是将一个系统看作是多个对象的集合,一个对象包含了特定的方面和特定的方法,他可以提供一个公共的接口给他人使用,而不用关心内部如何实现

比如我们想要创建一个学校,这个学校里有很多个教授对象,而这些教授都有名字和他们所教授的科目这两个属性,同时他们还具有自我介绍和给学生打分的能力,因此这个教授概念就可以抽象为一个教授Professor类Professor类规定了所有教授都有的属性property方法method

而基于这个类我们可以创建出一个具体的教授,这就是实例对象

面向对象主要包括三个概念多态、继承、封装

多态,一个方法在不同的类中具有不同的作用

继承,一种类可能是另一个类的子级。而此时子级可以继承父级的方法和属性,比如教授和学生都是两个类,但同时他们都继承了人类这个类的方法和属性

封装,将对象内部的操作封装成方法,对外只提供使用接口,这样保证了内部的私有性被称为封装

面向对象编程一般是用在大项目中,优点是减少了冲突,避免了很多重复的工作,且使用者不需要关心对象内部如何实现

JavaScript中的面向对象

构造函数

面向对象的核心思想是要让方法变得可以继承减少代码耦合,因此我们需要想办法将一个公共方法变得人人都可以使用,这时候就需要构造函数了,他是JavaScript中的类定义,使用构造函数我们可以创建一个对象,这个对象就拥有了构造函数定义的属性和方法

具体是如何实现的呢,在JavaScript中所有对象都具有一个共同的属性[[prototype]],他被称为隐式原型(这只是一种文档上的规范写法,实际上各个厂商是使用__proto__来实现访问的),而隐式原型[[prototype]]指的就是构造自身的构造函数的原型对象,既然是对象,自然也有隐式原型指向他的构造函数的原型对象,这一系列被称为原型链,而原型链终于以null原型对象的对象上,当然,这里我们只需要了解即可

而JavaScript中所有函数(包括构造函数)都具有prototype属性被称为原型属性,这里的prototype属性不要和上文的隐式原型[[prototype]]混淆,前者是函数独有的属性,指向的是自身的原型对象,因此当函数变成构造函数时,他的原型属性和他所构造的对象的隐式原型指向的是同一个原型对象

基于原型对象这一概念,我们便可以将我们需要公共使用的方法添加到原型对象上,这样以具有这个原型对象的函数作为构造函数的所有对象就都被赋予该原型对象中的方法了

function Person(name, age) {
    this.name = name
    this.age = age
}
Person.prototype.sayHi = function () {
    console.log('hello')
}

let p = new Person('jack',18)
console.log(p.__proto__  === Person.prototype)  //true

上例中我们创建了一个Person构造函数,在JS中构造函数默认应该以大写开头,它初始化了名字和年龄,然后我们将sayHi方法加到了这个构造函数的prototype属性所指的原型对象中,它的作用是打印一个hello

这样以这个构造函数构造的对象就将获得Person.prototype为其原型对象,因此也具有了sayHi这个方法

而我们让对象实例的隐式原型和构造函数的原型属性所指的

自有属性

我们使用上面的 Person 构造函数创建的对象有三个属性:

  • name 属性,在构造函数中设置,在 Person 对象中可以直接看到
  • age属性,在构造函数中设置,在 Person 对象中可以直接看到
  • sayHi() 方法,在原型中设置

我们经常看到这种模式,即方法是在原型上定义的,但数据属性是在构造函数中定义的。这是因为方法通常对我们创建的每个对象都是一样的,而我们通常希望每个对象的数据属性都有自己的值(就像这里每个人都有不同的名字和年龄)

直接在对象中定义的属性,如这里的nameage,被称为自有属性

构造函数的使用

和很多语言一样,我们使用new关键字来构造一个对象

const p = new Person('Abel', 20)
const p2 = new Person('Jack', 18)

上例中我们创建了两个对象,这两个对象具有自己的自有属性,我们修改任意一个对象的属性不会影响另一个对象,因此这是两个不同的对象,但同时两者又具有相同的方法sayHi

console.log('看看俩对象相不相等', p == p2)  //false
console.log('看看俩对象的方法相不相等', p.sayHi == p2.sayHi)  //true

ES6新增面向对象

class

上述创建构造函数方法可能较为繁琐,ES6有一个新的更简便的创建构造函数的方法,和java一样使用class关键字

class Person {
    name;  //自有属性声明,可以省略
    age;
    constructor(name,age) { //一个需要参数的构造函数,参数用来初始化新的对象的自有属性
        this.name = name;  //初始化自有属性,没有声明会自动声明,因此前面的声明可以省略
        this.age = age;
    }     
    sayHi() { // 方法
        console.log('hello')
    }     
    abc() { }       // 方法
    aaa() { }       // 方法
    bbb() { }       // 方法
}
const p = new Person()        // ES6 类只能使用 new关键字执行
p.sayHi();  // hello

class关键字创建的构造函数只能使用new来使用,如果是使用函数书写的构造函数不使用new就变成了赋值函数,而class不使用new会报错

class内属性的声明可以省略,因为构造函数在初始化自有属性时如果没定义会先定义

方法则直接和constructor函数同级书写

继承

如果我们创建的类(构造函数)需要继承父级类的属性和方法该如何书写呢

比如我们创建一个教授professor子类,他是基于Person这个类的

class Professor extends Person {  //继承person
      teaches;
      constructor(name, age,teaches) {
        super(name);  //继承父级的属性
        super(age);
        this.teaches = teaches;
      }

      introduceSelf() {
        console.log(
          `My name is ${this.name}, and I will be your ${this.teaches} professor.`,
        );
      }

      grade(paper) {
        const grade = Math.floor(Math.random() * (5 - 1) + 1);
        console.log(paper + grade);
      }
    }

    const walsh = new Professor("Walsh", 36,"Psychology");
    walsh.introduceSelf(); // 'My name is Walsh, and I will be your Psychology professor'
    walsh.grade("my paper"); // some random grade
    walsh.sayHi() // hello

我们在创建构造函数的时候使用extends关键字来声明本构造函数继承自的父类,此时通过子类创建的对象就可以使用父类的方法了,而构造函数内可以使用super(属性)来继承父类的属性了,这样通过子类创建的对象也可以初始化继承自父类的属性

私有属性和私有方法

以上所有类创建的对象的内部属性和方法在外部都可以被读取到,而如果我们希望一个属性或者方法不能被对象外读取到只在类内使用则需要变成私有属性或私有方法,在属性或方法前加#

class Student extends Person {
      #year;

      constructor(name, year) {
        super(name);
        this.#year = year;
      }

      introduceSelf() {
        console.log(`Hi! I'm ${this.name}, and I'm in year ${this.#year}.`);
      }

      canStudyArchery() {
        return this.#year > 6;
      }

      #myID(){
          let id = 345454534356435
          console.log(id)
     }
    }

    const summers = new Student("Summers", 22);
    summers.introduceSelf(); // Hi! I'm Summers, and I'm in year 22.
    console.log(summers.canStudyArchery()); // true
    console.log(summers.name)  //可以读取到
    summers.#myID() //报错
    console.log(summers.#year)  //报错

此时从外部读取私有属性和私有方法会报错

练习

放大镜

html

  <div class="boxSmall">
    <img src="QQ图片20240411154057.jpg" alt="" class="small">
    <div class="mask"></div>
  </div>
  <div class="boxBig">
    <img src="QQ图片20240411154032.jpg" alt="" class="big">
  </div>

css

  <style>
    * {
      margin: 0;
      padding: 0;
    }

    .boxSmall {
      width: 300px;
      height: 300px;
      border: 2px solid #ccc;
      float: left;
      position: relative;
      margin-left: 20px;
      margin-top: 20px;
    }

    .small {
      width: 300px;
      height: 300px;
    }


    .boxBig {
      width: 600px;
      height: 600px;
      overflow: hidden;
      float: left;
      display: none;
    }

    .mask {
      width: 150px;
      height: 150px;
      position: absolute;
      background: rgba(239, 140, 140, .3);
      left: 0;
      top: 0;
      display: none;
    }
  </style>

js

  <script>
    class Zoom {
      constructor(option) {
        this.mask = document.querySelector(option.mask)
        this.boxSmall = document.querySelector(option.boxSmall)
        this.imgBig = document.querySelector(option.imgBig)
        this.imgSmall = document.querySelector(option.imgSmall)
        this.boxBig = document.querySelector(option.boxBig)
        this.move()
      }
      move() {
        this.boxSmall.onmouseover = e => {
          this.mask.style.display = 'block'
          this.boxBig.style.display = 'block'
            //根据小图片和遮罩的大小比例来设定大图片大小,让小图片大小/遮罩大小=大图片大小/大盒子大小
          // 属性的宽高不带px,getComputedStyle获取到的是字符串,带px
          this.imgBig.width = (this.imgSmall.offsetWidth / parseFloat(getComputedStyle(this.mask).width) * parseFloat(getComputedStyle(this.boxBig).width))
          this.imgBig.height = (this.imgSmall.offsetHeight / parseFloat(getComputedStyle(this.mask).height) * parseFloat(getComputedStyle(this.boxBig).height))
          this.boxSmall.onmousemove = e => {
              //获取到鼠标距离盒子边框的距离
            const disX = e.pageX - this.boxSmall.offsetLeft
            const disY = e.pageY - this.boxSmall.offsetTop
            //计算遮罩应该距离边框的距离,同时让遮罩处于元素中心
            let x = disX - this.mask.offsetWidth / 2
            let y = disY - this.mask.offsetHeight / 2
            //限制遮罩不要出界
            if (x <= 0) {
              x = 0
            } else if (x >= this.boxSmall.clientWidth - this.mask.offsetWidth) {
              x = this.boxSmall.clientWidth - this.mask.offsetWidth
            }
            if (y <= 0) {
              y = 0
            } else if (y >= this.boxSmall.clientHeight - this.mask.offsetHeight) {
              y = this.boxSmall.clientHeight - this.mask.offsetHeight
            }
              //设置遮罩的位置
            this.mask.style.left = x + 'px'
            this.mask.style.top = y + 'px'
            //根据遮罩距离边框的距离计算大图对于大框的位置,这里乘的是大小图片的比例
            this.imgBig.style["marginLeft"] = -x * (this.imgBig.offsetWidth / this.imgSmall.offsetWidth) + 'px'
            this.imgBig.style["marginTop"] = -y * (this.imgBig.offsetHeight / this.imgSmall.offsetHeight) + 'px'
          }
          e.preventDefault()
        }

        this.boxSmall.onmouseout = e => {
          this.boxSmall.onmousemove = ''
          this.mask.style.display = 'none'
          this.boxBig.style.display = 'none'
        }
      }
    }

    let z = new Zoom({
      mask: '.mask',
      boxSmall: '.boxSmall',
      boxBig: '.boxBig',
      imgBig: '.big',
      imgSmall: '.small'
    })
  </script>

练习中遇到的问题

使用getComputedStyle获取到的数值是带单位的字符串,需要转成数字才能使用

在图片设置为display:'none'offsetWidth无法读取到数值

元素属性的宽高是不需要带单位的


Day3—闭包、防抖和节流

闭包

形式就是函数套函数,此时内部函数和变量就组成了闭包,在实际开发中并不需要有意去了解

闭包的作用是可以在外部访问外层函数的局部变量,使得外层函数调用完成后不会回收内部变量

闭包耗费性能,而且使用不当可能会造成内存泄漏,因为外层函数的变量一直存在无法利用

我们事实上经常在使用闭包,只要函数套函数,本质都是闭包

懒加载

懒加载,在需要加载多个元素的网页上常用

因为一下将多个元素全部加载完毕及其耗费性能,而且会导致渲染延迟,因此使用懒加载来加强体验

懒加载的原理是,当一个元素距离顶部的距离小于可视窗口的高度加可视窗口滑过的距离,就说明元素已经进入可视窗口了,此时再对这个元素进行加载

const oImg = document.querySelectorAll('img')
    function img() {
      const i = innerHeight
      const t = scrollY
      oImg.forEach(item => {
        if (item.offsetTop < i + t) {
          item.src = item.getAttribute('_src')
        }
      })
    }
    img()
    window.onscroll = () => {
      img()
    }

上例中先将图片地址存放在自定义属性中,在应该加载的时候将图片地址赋予路径属性

防抖

防抖,一定时间内多次触发视为一次执行,期间每次触发都会导致计时器重置,只有一段时间内无操作使计时器结束后方可进行下一次执行,因此如果一直触发导致计时器一直重置则永远不会进行下一次执行

    const oBtn = document.querySelector('button')
    function fn() {
      let timer = null
      let flag = true
      oBtn.onclick = () => {  
        if (flag) {
          console.log('点击')
        }
        clearInterval(timer)
        timer = setInterval(() => {
          flag = true
        }, 1000)
        flag = false

      }
    }
    fn()

可以选择首次立即执行一次也可以选择不立即执行

节流

节流,固定时间内连续触发只执行一次,期间多次执行不重置计时器,因此如果一直触发,则只在固定时间会执行一次

    function scroll() {
      let flag = true
      let timer = null
      window.onscroll = () => {  
        if (flag) {
          console.log(1)
          clearInterval(timer)
          timer = setInterval(() => {  //多是使用Ajax而非定时器,当数据准备完成则加载
            flag = true
          }, 500)
        }
        flag = false
      }
    }
    scroll()

在代码思路上两者差别非常小,两者都是为了减少多次触发导致的性能占用


Day4—杂项

合并对象

对于后端传过来的多个数据我们有时需要合并,因此需要合并对象的方法

一种是利用浅拷贝

const obj = {
    name: 'jack'
}
const obj2 = {
    age: 18
}
console.log({ ...obj, ...obj2 })

一种是利用assign方法

console.log(Object.assign(obj, obj2))

两者效果是一样的

对象使用数组方法

将对象转成数组,使用.keys()方法,它是将关键字转成数组

        Object.keys(obj).map((key, index) => {
            console.log(obj[key], index)
        })

异步、同步和事件

JS在运行的时候是根据解释器来自动分析代码的执行顺序的

如果是一个普通语句则是同步执行

如果是一个定时器,则是异步执行,因为JS不可能等待计时器完成后才进行其他计算

如果是一个I/O事件,则是什么时候触发什么时候执行,因为我们不知道用户会什么时候触发事件

        document.onclick = function () {
            console.log('Click')
        }
        setTimeout(() => {
            console.log('timeout')
        }, 0)
        console.log('同步')

// 执行循序是  同步-->timeout-->Click

回调函数

基于上述的异步原理,有些函数应该在适当的时候才执行,比如事件触发后,如果直接在全局调用函数,那么JS解释器并不会跳过函数调用

因此将函数作为参数传入另一函数内,然后在这个函数内调用函数被称为回调

回调函数一般用在return无法使用的时候,比如计时器、事件内,回调函数可以把数据取回来

回调函数和闭包有区别

        function getData(fn) {
            var res = null
            setTimeout(() => {
                res = [12, 5, 7]
                fn(res) // 必须得到数据的时候才执行函数
            }, 300)
        }
        getData(function (res) {
            console.log(res)
        })

this指向

this指向

this指向的都是对象,在调用的那一刻点前面是谁this就指向谁

指向对象的情况有,实例化函数、普通对象、事件对象

指向window的情况有,定时器、计时器、mapsomeeveryfilter和回调函数

箭头函数没有this,他使用的是父级的this

严格模式下指向windowthis全部变成undefined

        // obj调用普通函数,则this指向obj
        const obj = {
            name: 'Jack',
            fn() {
                console.log(this)   // obj
            }
        }
        obj.fn()
        // obj2调用箭头函数,箭头函数没有this,因此使用obj的this,是window
        const obj2 = {
            name: "Jack",
            fn: () => {
                console.log(this)   // widnow  如果执行的函数是箭头函数,this还是会指向上一层
            }
        }
        obj2.fn()
        // 在obj3中调用show()函数前面没有任何对象,因此是window
        const obj3 = {
            name: 'jack',
            fn() {
                console.log(this)   // obj3
                function show() {
                    console.log(this)       // window
                }
                show()
            }
        }
        obj3.fn()
        // show函数是箭头函数他没有this,因此他的this用的是父级也就是fn的this,因此指向obj4
        const obj4 = {
            name: 'jack',
            fn() {
                console.log(this)   // obj4
                const show = () => {
                    console.log(this)       // obj
                }
                show()
            }
        }
        obj4.fn()

改变this指向

函数自带的三个改变this指向的方法

函数.call(目标数据),将函数内this指向改为目标数据并执行函数

函数.apply(目标数据),将函数内this指向改为目标数据并执行函数

函数.bind(目标数据),将函数内this指向改为目标数据,但不执行函数,当在后面再加一个圆括号调用时才执行函数

第三种方法较为推荐,因为他默认不执行函数

什么时候会改变this指向

当没有继承关键字时,通过将父类的this改为子类实现继承

        function Person(name, age) {
            this.name = name
            this.age = age
        }
        const p = new Person('张三', 18)
        console.log(p)

        function Son(name, age) {
            let self = this
            Person.call(self, name, age)
            // Person.apply(self, [name, age])
            // Person.bind(self)(name, age)
        }
        const s = new Son('狗子', 2)
        console.log(s)

柯里化函数

本质是闭包的另一种形式

在函数作为返回值返回后,加上那个圆括号来立即调用

        function fn() {
            console.log('外层函数')
            return function () {
                console.log('内层函数')
            }
        }
        fn()()

数据类型检测

typeof只能检测基本数据类型,对于复杂数据类型全部是对象

数据 instanceof 数据类型

真正严格的数据类型检测是Object.prototype.toString.call(数据)

ES6+新增

数组操作

arr.reduce((a, item) => a + item),数组操作,a是数组第一个元素,item是数组剩余元素,用第一个元素对剩余元素循环调用函数操作

arr.find(item => item > 50),返回符合条件的第一个内容

arr.findIndex(item => item > 50),返回符合条件的第一个内容的下标

arr.flat(),二维数组转一维数组

对象操作

str.trim(),字符串方法,去除首尾空格

str.trimStart(),字符串方法,去除首空格

str.trimEnd(),字符串方法,去除尾空格

str.startsWith(),在字符串前面加内容

str.endWith(),在字符串后面加内容

str.valueOf(),把构造对象转为字符串

?.,可选链操作符,新增对象方法,在对象内没有相应属性时返回undefined,而非报错,使用格式为obj?.info?.id

杂项

BigInt(),可以使用超16位大数,在JS中超过16位的数字会显示错误

沙箱模式

监听属性变化,在别人修改对象内的属性时可以通过沙箱来截取操作

同时使用settergetter实现

setter定义一个伪属性,直接获取伪属性返回undefined,他可以接受一个为伪属性设置的数值作为参数,在设置伪属性时,可以将对象已存在的属性绑定到设置伪属性时要调用的函数上

getter定义一个伪属性,更改伪属性的数值无法生效,在查询伪属性时,他将对象已有的属性绑定到查询伪属性时调用的函数上

        var n = 100
        var obj = {
            b:50,
            get a() {
                return this.b
            },
            set a(val) {
                if (typeof val != 'number') {
                    this.b = '不是数字'  // 不是数字提示文字
                } else {
                    this.b = val     // 是数字正常显示
                }
            }
        }
        console.log(obj.a)
        obj.a = 200  // {a:200}
        obj.a = 300  // {a:300}
        obj.a = '400'  // {a:不是数字}
        console.log(obj)

上例中使用settergetter设置了伪属性a,他们各自的函数分别绑定了对象已有的属性b

当查询伪属性a时,触发get对应的函数,返回对象的b属性

设置伪属性a时,触发set对应的函数,判断参数是否是数值然后给对象的属性b赋值

在实践中的一种用法是

        var obj = {
            var n = 100
            get a() {
                return n
            },
            set a(val) {
                if (typeof val != 'number') {
                    n = '不是数字'  // 不是数字提示文字
                } else {
                    n = val     // 是数字正常显示
                }
            }
        }
        obj.a = 200  // {a:200}
        obj.a = 300  // {a:300}
        obj.a = '400'  // {a:不是数字}
        console.log(obj)

实际上这种用法和上一种用法几乎没区别,因为从始到终操控的值都是n而非伪属性a

数据劫持

Object.defineProperty(),在vue2中使用,已经是过去式了

        const obj = {
            age: 10
        }
        var n = null
        Object.defineProperty(obj, 'age', {
            set(val) {
                // console.log(val)    // 已经被劫持到了
                if (typeof val == 'number') {
                    n = val
                } else {
                    n = '非数字类型'
                }
            },
            get() {
                return n
            }
        })
        obj.age = 1000 //{age:1000}
        obj.age = 200  //{age:200}
        obj.age = '123' //{age:非数字类型}
        console.log(obj)

数据代理

let obj2 = new Proxy(代理对象,捕获器)

里面有setget方法,和捕获器差不多,但要区分开

get方法,捕获对对象的任何属性的读取操作,参数分别是,源对象、被读取的目标属性、proxy实例对象对象

set方法,捕获对对象的任何属性的设置操作,参数分别是,源对象、将被设置的属性名、设置的属性值和最初接受赋值的对象

主要用来使两个不同的对象互相绑定,一个数值修改则另一个数值也自动修改

        let obj = {
            name: 'Jack',
            age: 18
        }
        var n = null
        let obj2 = new Proxy(obj, {  //数据代理,obj2代理obj
            get(oldObj, key, newObj) {
                console.log('get')
                return n
            },
            set(oldObj, key, value, newObj) {
                if (typeof value == 'number') {
                    oldObj[key] = value
                } else {
                    n = '非数字类型'
                }
            }
        })

        obj2.age = 20
        obj2.age = '21'
        obj.age = 111

        console.log(obj, obj2)  //两者属性相同
        console.log('两个对象是否相等', obj == obj2)  // false,表示两者不是一个对象

严格模式

严格模式下不规范语句会报错,严格按照标准执行

使用方法是在JS文件开头书写'use strict'