💦 提升的目的
将函数的声明定于与变量的声明进行提升,可以提前为函数和变量预留内存空间,预留比实际所需略大的内存空间,减少内存重新分配的次数,从而提高性能。
🌊 立即执行函数(IIFE)
在开发中应该尽量减少直接在全局作用域中编写代码,在全局作用域声明的变量容易被修改或覆盖,所以我们的代码要尽量编写的局部作用域。
如果使用let声明的变量,可以使用{}来创建块作用域,不同块级作用域的同名变量不会造成干扰,var声明的变量不具有块级作用域,使用var关键字声明的不同块级作用域下的同名变量会互相影响。如果使用var声明的变量,可以使用函数作用域
创建一个只执行一次的匿名函数,立即执行函数(IIFE),立即执行函数是一个匿名的函数,并它只会调用一次,可以利用IIFE来创建一个一次性的函数作用域,避免变量冲突的问题
// 括号内的函数虽然为使用function创建的函数,但是没有函数名 // 所以该函数不会被提升 // 以括号开头的函数不会被提升 (function(){ let a = 10 console.log(111) }()); // 如果没有添加分号会两个立即执行函数会被解释器认为(...)(...) // Js解析器自动添加的分号会在第二个立即执行函数后面 (...)(...); // 为了避免报错,可以手动添加分号 // (intermediate value)(intermediate value)(...) is not a function // 调用立即执行函数的括号可以写在外面也可以写在里面 (function(){ let a = 20 console.log(222) })()
🌊 this
函数在执行时,JS解析器每次都会传递进一个隐含的参数,这个参数就叫做 this,this会指向一个对象,this所指向的对象会根据函数调用方式的不同而不同。
以函数形式调用时,this指向的是window;以方法的形式调用时,this指向的是调用方法的对象
function fn() { console.log(this === window) console.log("fn打印", this) } const obj = { name: "孙悟空" } obj.test = fn const obj2 = { name: "猪八戒", test: fn } fn() // 方法中的this为调用方法的对象 // function声明的函数为window对象的方法,直接调用函数相当于window.fn() // 相当于调用window上的方法,所以以函数形式调用时,this指向的是window window.fn() obj.test() // {name:"孙悟空"} obj2.test() // {name:"猪八戒", test:fn}
通过this可以在方法中引用调用方法的对象,从而在方法中可以使用对象的属性或方法。
const obj3 = { name: "沙和尚", sayHello: function () { console.log(this.name) }, } const obj4 = { name: "唐僧", sayHello: function(){ console.log(this.name) } } // 为两个对象添加一个方法,可以打印自己的名字 obj3.sayHello() obj4.sayHello()
🌊 箭头函数的this
- 箭头函数的语法:
([参数]) => 返回值
- 无参箭头函数:
() => 返回值
- 一个参数的:
a => 返回值
- 多个参数的:
(a, b) => 返回值
- 只有一个语句的函数:
() => 返回值
- 只返回一个对象的函数:
() => ({...})
- 有多行语句的函数:
() => { .... return 返回值 }
- 箭头函数没有自己的this,它的this有外层作用域决定
- 箭头函数的this和它的调用方式无关
function fn() { console.log("fn -->", this) } // 箭头函数没有自己的this,它的this有外层作用域决定 // 由于该箭头函数声明在全局中,所以该箭头函数的this总是window const fn2 = () => { console.log("fn2 -->", this) } fn() // window fn2() // window const obj = { name:"孙悟空", fn, // fn:fn 属性名和属性值一样可以简写 fn2, // 箭头函数fn2声明在全局作用域中,this指向window sayHello(){ console.log(this.name) function t(){ console.log("t -->", this) } t() // 以函数形式调用时,this指向的是window // 当前箭头函数声明在对象的sayHello方法中 // 当前作用域的this为调用该方法的对象 const t2 = () => { console.log("t2 -->", this) } t2() } } obj.fn() // obj obj.fn2() // window obj.sayHello()
🥽 严格模式
- JS运行代码的模式有两种:
- 正常模式
- 默认情况下代码都运行在正常模式中,在正常模式,语法检查并不严格
- 它的原则是:能不报错的地方尽量不报错
- 这种处理方式导致代码的运行性能较差
- 严格模式
- 在严格模式下,语法检查变得严格
- 禁止一些语法
- 更容易报错,有些正常模式下不报错的在严格模式下会报错,如
a = 10
即直接给变量a赋值没有声明,在正常模式下不报错,但是在严格模式下会报错 - 提升了性能
- 在开发中,应该尽量使用严格模式,这样可以将一些隐藏的问题消灭在萌芽阶段,同时也能提升代码的运行性能
- 开启严格模式
- 全局严格模式,在js代码的开头(js代码第一行),写如下代码开启全局严格模式,
"use strict"
"use strict" // 全局的严格模式 a = 10
- 只在函数中开启严格模式
function fn(){ "use strict" // 函数的严格的模式 ... }
🥽 面向对象
🌊 面向对象概述
- 面向对象编程(OOP)
- 程序是干嘛的?
- 程序就是对现实世界的抽象
- 对象是干嘛的?
- 一个事物抽象到程序中后就变成了对象
- 在程序的世界中,一切皆对象
- 面向对象的编程
- 面向对象的编程指,程序中的所有操作都是通过对象来完成
- 做任何事情之前都需要先找到它的对象,然后通过对象来完成各种操作
- 一个事物通常由两部分组成:数据和功能
- 一个对象由两部分组成:属性和方法
- 事物的数据到了对象中,体现为属性
- 事物的功能到了对象中,体现为方法
const five = { // 添加属性 name:"王老五", age:48, height:180, weight:100, // 添加方法 sleep(){ console.log(this.name + "睡觉了~") }, eat(){ console.log(this.name + "吃饭了~") } }
🌊 类
- 使用Object或对象字面量
{}
创建对象的问题:
- 无法区分出不同类型的对象
- 不方便批量创建对象
- 在JS中可以通过类(class)来解决这个问题:
- 类是对象模板,可以将对象中的属性和方法直接定义在类中,定义后,就可以直接通过类来创建对象。类好比汽车制作的图纸,每个汽车为汽车这个类对应的汽车对象。
- 通过同一个类创建的对象,我们称为同类对象,可以使用
instanceof
来检查一个对象是否是由某个类创建,如果某个对象是由某个类所创建,则我们称该对象是这个类的实例。
- 类是创建对象的模板,要创建对象第一件事就是定义类,定义类的语法:
class 类名 {} // 类名要使用大驼峰命名 const 类名 = class {}
- 通过类创建对象
new 类()
// Person类专门用来创建人的对象 class Person{ } // Dog类式专门用来创建狗的对象 class Dog{ } // 使用类创建对象,便于批量创建对象 const p1 = new Person() // 调用类的构造函数创建对象 const p2 = new Person() console.log(p1, p2) // 使用类创建对象,可以区分出不同类型的对象 const d1 = new Dog() const d2 = new Dog() console.log(d1, d2) console.log(p1 instanceof Person) // true console.log(d1 instanceof Person) // false
🌊 属性
类构造出来的对象,传统的属性添加方法
class Person {} const p1 = new Person() p1.name = 'Tom' console.log(p1)
类的代码块,默认就是严格模式,类的代码块是用来设置对象的属性和方法的,不是什么代码都能写
class Person { // Person的实例属性 // 在类中定义了类的实例属性,通过该类创建出来的对象都会具有一个属于自己的该属性 // 实例属性只能通过实例访问 // 调用实例属性 对象名.实例属性 name = "孙悟空" // Person的实例属性name,并附初始值,也可以不赋初始值,则默认初始值为undefined age = 18 // 使用static声明的属性,是静态属性(类属性) // 静态属性只能通过类去访问 // 调用静态属性 类名.静态属性 static hh = "静态属性" } const p1 = new Person() p1.name = 'Tom' // 访问p1的实例属性name console.log(p1) console.log(Person.hh) // 访问Person的类属性
🌊 方法
class Person { // 实例属性 name = "Tom" // 方法 // 方法其实也是属性,只是方法的值为函数 // 添加方法的方式一 sayHi = function() { // 实例方法 console.log('hi world') } // 添加方法的方式二 // 添加方法可以直接写 `方法名+参数列表+方法体` sayHello() { // 实例方法 console.log('hello world') } // 实例方法的调用 对象.方法() // 实例方法中的this为调用方法的实例对象 printThis() { console.log('实例方法的this: ', this) } // 静态方法(类方法) // 通过类来调用 类.方法() // 静态方法中this指向的是当前类 static printStaticThis() { console.log('静态方法的this: ', this) } } const p1 = new Person() console.log(p1) p1.sayHi() p1.sayHello() p1.printThis() Person.printStaticThis()
🌊 构造函数
在类中可以添加一个特殊的方法constructor,该方法我们称为构造函数(构造方法),构造函数会在我们调用类创建对象时执行,我们可以在构造函数中,为实例属性进行赋值。
在构造函数中,this表示当前所创建的对象。
class Person { constructor(name, age, gender) { console.log("构造函数执行了~", name, age, gender) // this指向当前创建出来的对象 // 向创建出来的对象中添加属性name,其值为通过参数传递过来的name // 对象的属性可以动态添加 this.name = name this.age = age this.gender = gender } } const p1 = new Person('孙悟空', 18, '男') const p2 = new Person('猪八戒', 28, '男') const p3 = new Person('沙和尚', 38, '男') console.log(p1) console.log(p2) console.log(p3)
🌊 面向对象的三大特性
- 面向对象的三大特性:
- 封装 —— 安全性
- 继承 —— 扩展性
- 多态 —— 灵活性
💦 封装
- 对象就是一个用来存储不同属性的容器
- 对象不仅存储属性,还要负责数据的安全,即还要保证数据的合法性
- 直接添加到对象中的属性,并不安全,因为它们可以被任意的修改
- 如何确保数据的安全:
- 私有化数据
- 将需要保护的数据设置为私有,只能在类内部使用
- 实例属性使用
#
开头就变成了私有属性,私有属性只能在类内部访问
- 提供setter和getter方法来开放对数据的操作
- getter方法,用来读取属性
- setter方法,用来设置属性
- 属性设置私有,通过getter setter方法操作属性带来的好处
- 可以控制属性的读写权限
- 可以在方法中对属性的值进行验证
- 封装主要用来保证数据的安全
- 实现封装的方式:
- 属性私有化加
#
- 通过getter和setter方法来操作属性
// getter与setter使用下面的写法 // 调用属性和修改属性可以使用 对象.属性 // 但是在调用属性和修改属性时会自动调用相应的getter和setter方法 get 属性名(){ return this.#属性 } set 属性名(参数){ this.#属性 = 参数 }
class Person { // 实例使用#开头就变成了私有属性,私有属性只能在类内部访问 #name #age #gender constructor(name, age, gender) { this.#name = name this.#age = age this.#gender = gender } sayHello() { console.log(this.#name) } // getter方法,用来读取属性 getName(){ return this.#name } // setter方法,用来设置属性 setName(name){ this.#name = name } getAge(){ return this.#age } setAge(age){ // 校验数据是否合法 if(age >= 0){ this.#age = age } } // getter与setter使用下面的写法 // 调用属性和修改属性可以使用 对象.属性 // 但是在调用属性和修改属性时会自动调用相应的getter和setter方法 get gender(){ return this.#gender } set gender(gender){ this.#gender = gender } } const p1 = new Person("孙悟空", 18, "男") console.log(p1.getName()) // -11 修改的数据不合法,在setter方法中判断不合法不会继续修改,保证了数据的合法 p1.setAge(-11) p1.gender = "女" // getter与setter使用 `get 属性` `set 属性` 写法的调用 console.log(p1.gender) console.log(p1)
💦 多态
多态,在JS中不会检查参数的类型,所以这就意味着任何数据都可以作为参数传递,要调用某个函数,无需指定的类型,只要对象满足某些条件即可。即调用某个函数不关心函数参数的类型,关心函数的参数是否具有某些特点。
多态为我们提供了灵活性。
class Person{ constructor(name){ this.name = name } } class Dog{ constructor(name){ this.name = name } } class Test{ } const dog = new Dog('旺财') const person = new Person("孙悟空") const test = new Test() function sayHello(obj){ if(obj.hasOwnProperty('name')){ // 判断对象是否具有name属性 console.log("Hello,"+obj.name) } } sayHello(dog) sayHello(person) sayHello(test)
💦 继承
- 可以通过extends关键来完成继承
- 当一个类继承另一个类时,就相当于将另一个类中的代码复制到了当前类中(简单理解)
class Animal{ constructor(name){ this.name = name } sayHello(){ console.log("动物在叫~") } } class Dog extends Animal{ } class Cat extends Animal{ } const dog = new Dog("旺财") const cat = new Cat("汤姆") dog.sayHello() cat.sayHello()
- 继承发生时,被继承的类称为 父类(超类),继承的类称为 子类
- 通过继承可以减少重复的代码,并且可以在不修改一个类的前提对其进行扩展
- 在子类中,可以通过创建同名方法来重写父类的方法
class Dog extends Animal{ // 在子类中,可以通过创建同名方法来重写父类的方法 sayHello(){ console.log("汪汪汪") } } const dog = new Dog("旺财") const cat = new Cat("汤姆") dog.sayHello() cat.sayHello()
- 重写构造函数时,构造函数的第一行代码必须为super(),super表示父类,super()调用父类的构造函数
class Cat extends Animal{ // 重写构造函数 constructor(name, age){ // 重写构造函数时,构造函数的第一行代码必须为super() super(name) // 调用父类的构造函数 this.age = age } sayHello(){ // 调用一下父类的sayHello super.sayHello() // 在方法中可以使用super来引用父类的方法 console.log("喵喵喵") } } const dog = new Dog("旺财") const cat = new Cat("汤姆", 3) dog.sayHello() cat.sayHello() console.log(dog) console.log(cat)
- 通过继承可以在不修改一个类的情况下对其进行扩展
- OCP 开闭原则,程序应该对修改关闭,对扩展开放
🌊 对象的结构
- 对象中存储属性的区域实际有两个:
- 对象自身
- 直接通过对象所添加的属性,位于对象自身中
- 在类中通过 x = y 的形式添加的属性,位于对象自身中
class Person { name = "孙悟空" // 在类中通过 x = y 的形式添加的属性 age = 18 // 在类中通过 x = y 的形式添加的属性 constructor(){ this.gender = "男" // 通过对象所添加的属性 } } const p = new Person() p.address = "花果山" // 通过对象所添加的属性
- 原型对象(prototype)
- 对象中还有一些内容,会存储到其他的对象里(原型对象)
- 在对象中会有一个属性用来存储原型对象,这个属性叫做__proto__
- 原型对象也负责为对象存储属性,当我们访问对象中的属性时,会优先访问对象自身的属性,对象自身不包含该属性时,才会去原型对象中寻找
- 会添加到原型对象中的情况:
- 在类中通过xxx(){}方式添加的方法,位于原型中
- 主动向原型中添加的属性或方法
🌊 原型对象
💦 访问一个对象的原型对象
// 方式一 对象.__proto__ // 不推荐 // 方式二 Object.getPrototypeOf(对象)
class Person { name = "孙悟空" age = 18 sayHello() { console.log("Hello,我是", this.name) } } const p = new Person() console.log(p) // 一般以下划线开头的属性都是不希望直接进行访问的属性 console.log(p.__proto__)
const p = new Person() console.log(p) // 一般以下划线开头的属性都是不希望直接进行访问的属性 console.log(p.__proto__) console.log(Object.getPrototypeOf(p)) // 获取某个对象的原型对象
由结果可以看出,原型对象中的数据包含:1. 对象中的数据(属性、方法等)2. constructor (对象的构造函数,其实就是对象对应的类)
console.log(p.__proto__.constructor) console.log(p.constructor)