JavaScript 面向对象基础

面向对象基础


了解面向对象

面向对象
  • 面: 脸
  • 向: 朝着
  • 对象: 我们 JS 的数据类型
  • 脸朝着我们 JS 的数据类型
  • 学名: 关注对象的形式进行开发
  • 面向对象核心: 对面向过程的高度封装, 高内聚低耦合
  • 面向对象不是一个语法或者方法, 而是一个编程思想,是一个你怎么写代码
  • 也就是你怎么写代码, 怎么完成功能的一个思想
面向过程
  • 关注过程的开发, 我们以前写的选项卡, 九宫格, 轮播图都是面向过程的开发
    • 我要关注里面的每一个步骤
    • 获取哪些元素
    • 设置哪些元素
    • 先执行什么后执行什么
swiper 面向对象的开发
  • 关注对象的开发, 我们使用的 swiper 就是面向对象的开发
    • 直接找到一个能帮我完成功能的东西
    • 直接调用这个东西, 进行一些配置, 完成功能
  • 面向对象是不是不需要面向过程了?
    • 不是, 是因为有人把过程给我们封装好了
    • 所以我们可以直接使用
例子:我要吃一碗打卤面
  • 面向过程: 所有的步骤顺序细节都要考虑
    1. 和面 — 考虑多少水多少面粉
    2. 切面 — 多宽多细
    3. 煮面 — 煮熟了再吃
    4. 拌面 — 要和卤搅拌在一起, 拌多少卤
    5. 吃面 — 拿筷子吃, 还是拿手吃
  • 面向对象: 我们考虑的只有找不找得到一个面馆
    1. 找到一个面馆
    2. 点一碗面(把你的要求都说出来)
    3. 等着吃
面向对象的思想
  • 当我要吃 面 的时候, 找到一个 “面馆”

  • 如果能找到 “面馆”, 那么我直接点一碗面

  • 如果找不到 “面馆”, 那么我自己盖一个 “面馆”

  • 然后再点一碗面

  • 当你需要实现一个功能的时候, 找到有没有一个对象能帮我实现这个功能

    • 如果有, 那么直接拿过来使用

    • 如果没有, 那么我们创造一个 “机器”, 这个 “机器” 能帮我们制造一个完成功能的对象

    • 然后我们使用这个 “机器” 创造一个对象完成功能

    • “机器”: 就是把面向过程拿一套东西高度封装起来, 能创造一个完成功能的对象

      面向对象的核心: 对面向过程的高度封装, 高内聚低耦合


字面量创建对象

  • 使用字面量的形式创建一个对象
  • 创建的时候向里面添加一些内容
  • 也可以再后期动态添加
  • 缺点:这个东西不能量产
let obj = {
    name: 'Jack',
    age: 18,
    fn: function () {}
}

obj.gender = '男'
obj.fun = function () {}

console.log(obj) // {name: "Jack", age: 18, gender: "男", fn: ƒ, fun: ƒ}

// 当出现第二个的时候, 需要再写一遍
let obj2 = {
    name: 'Rose',
    age: 20
}

内置构造函数创建对象

  • 我们可以使用 内置构造函数 创建一个空对象
  • 动态象里面添加内容
  • 缺点:不能量产, 需要第二个, 那么一模一样的代码需要再写一遍
let obj = new Object()

obj.name = 'Jack'
obj.age = 18
obj.gender = '男'

console.log(obj) // {name: "Jack", age: 18, gender: "男"}

// 需要第二个对象
let obj2 = new Object()

obj2.name = 'Rose'
obj2.age = 20

console.log(obj2) // {name: "Rose", age: 20}

工厂函数创建对象

  1. 自己书写一个工厂函数
  2. 使用自己书写的工厂函数创建对象
  3. 什么是工厂函数?
    • 就是一个普通的函数, 只不过这个函数能给我返回出来一个对象
    • 我们给他起个名字叫做 工厂函数
// 1. 书写一个工厂函数
function createObj(name, age, gender) {
    // 手动创建一个对象
    let obj = {}

    // 手动添加成员
    obj.name = name
    obj.age = age
    obj.gender = gender

    // 手动返回这个对象
    return obj
}

// 2. 使用工厂函数创建对象
// 本次调用的时候, 传递进去的三个参数分别是 'Jack' 18 和 '男'
// 那么向对象里面添加的就是这三个数据
// 我传递的三个实参, 就是对象里面三个成员的值
let obj = createObj('Jack', 18, '男') 
console.log(obj) // // {name: "Jack", age: 18, gender: "男"}

// 创建第二个对象
// 本次调用的时候, 传递的是 Rose 20 和 女
// 那么对象里面三个成员的值就分别是我传递进去的东西
let obj2 = createObj('Rose', 20, '女')
console.log(obj2) // {name: "Rose", age: 20, gender: "女"}

自定义构造函数创建对象

  • 自定义一个构造函数
  • 使用自定义的构造函数创建对象
自定义构造函数
  • 一个自己写的函数, 就是一个函数
  • 当你使用的时候和 new 关键字连用, 就是构造函数
构造函数里面
  • 不需要我们手动创建对象, 他会自动创建
  • 不需要我们手动返回对象, 他会自动返回
  • 我们只需要手动向对象里面添加内容就可以了
  • 构造函数必须要和 new 连用
  • 在构造函数里面, this 指向当前创建的那个对象
    • 只要这个函数和 new 关键字连用了,我们叫做 构造函数 调用
    • 此时这个函数里面的 this 就指向本次调用的时候自动创建的对象
// 1. 书写一个自定义构造函数
// 构造函数也是函数, 只不过和 new 连用有了一个创造对象的能力
// 我们就给他起了一个 高端 一些的名字, 叫做构造函数而已
// 和普通函数的能力是一样的, 一样可以接收参数, 使用参数
function createObj(name, age) { // 自动创建对象
    // 手动向对象里面添加成员
    this.name = name
    this.age = age

    // 自动返回这个对象
}

// 2. 使用自定义构造函数创建对象
// 当一个函数和 new 关键字连用的时候, 就有自动创建对象的能力
// 本次调用的时候, 因为和 new 关键字连用了
// 所以构造函数会自动创建一个对象, 并且自动返回
// 我把返回的内容赋值给了 obj 这个变量
// 函数内部的 this 就是指向本次返回的对象
// 所以我写 this.name = 'Jack' 就是在向 obj 这个对象中添加成员
let obj = new createObj('Jack', 18) // new createObj 的时候, 这个函数需要执行一遍
console.log(obj) // createObj {name: "Jack", age: 18}

// 使用自定义构造函数创建第二个对象
let obj3 = new createObj('Rose', 20) // new createObj 的时候, 这个函数又需要执行一遍
console.log(obj3) // createObj {name: "Rose", age: 20}

// 当一个函数不和 new 关键字连用的时候, 那么没有自动创建对象的能力
let obj2 = createObj()
console.log(obj2) // undefined

// createObj 和 new createObj 调用的是一个函数
// 但是一个和 new 关键字连用了, 一个没有连用
// 和 new 连用的时候, this 指向本次调用创建出来的对象
// 没有和 new 连用的时候, 没有自动创建对象的能力, this -> window

在这里插入图片描述


构造函数的使用规则和规范

规范:建议你遵守, 因为大家都一样
  • 如果你写的是一个构造函数, 将来需要和 new 连用的时候

    • 建议你首字母大写, 没别的意思, 就是从名字上区分一下

      • function CreateObj2(name, age) {
            this.name = name
            this.age = age
        }
        
    • 内置构造函数

      • new Object()
      • new Array()
      • new Date()
      • new RegExp()
    • 第三方构造函数

      • new Swiper()
  • 当你使用构造函数的时候, 如果不需要传递参数

    • 那么结尾的小括号可以省略不写

    • 但是建议你不管是不是传递参数, 都最好写上

    • function Person() {
          this.name = 'Jack'
      }
      
      // 使用 Person 构造函数创建第一个对象
      let p1 = new Person()
      console.log(p1)
      // 使用 Person 构造函数创建第二个对象
      // 不传递参数的时候, 可以不写, 但是建议都写上
      let p2 = new Person
      console.log(p2)
      
规则:必须遵守,不遵守肯定是不对
  • 构造函数调用的时候, 必须要和 new 关键字连用

    • 因为构造函数的意义就是创造对象
    • 你不和 new 连用, 那么就没有自动创建对象的能力
    • 那么构造函数的意义就没有了
  • 构造函数内部的 this 指向本次调用创建的对象

    • 我们管这个对象叫做 实例对象

    • 我们管这个创造对象的过程叫做 实例化

    • 函数内部的 this 都是指向当前实例

    • function Person() {
          this.name = 'Jack'
      }
      
      // 本次调用的时候, Person 函数内部的 this 指向 p1
      let p1 = new Person()
      
      // 本次调用的时候, Person 函数内部的 this 指向 p2
      let p2 = new Person()
      
  • 构造函数内部不要写 return

    • 因为构造函数会自动 return 每次创建的对象

    • 所以你就不要再写 return 了

      • 如果你 return 了一个基本数据类型, 那么写了白写

      • 如果你 return 了一个复杂数据类型, 那么构造函数白写

      • function Person() {
            this.name = 'Jack'
            this.age = 18
            this.gender = '男'
        
            // return 基本数据类型
            // return 123 // 白写
            // return 'hello world' // 白写
            // return true // 白写
            // return undefined // 白写
            // return null // 白写
        
            // return 复杂数据类型
            // return [1, 2, 3] // 构造函数自动创建的对象, 不会被返回了
            // return function () {} // 构造函数自动创建的对象, 不会被返回了
            // return new Date() // 构造函数自动创建的对象, 不会被返回了
        }
        
        let p1 = new Person()
        console.log(p1)
        
  • 构造函数的意义: 就是用来创造对象的


构造函数不合理的地方

  • 直接在构造函数体内书写方法, 会造成内存的资源浪费
  • 写的越多浪费的越多
function Person() {
    this.name = 'Jack'
    this.age = 18
    this.fn = function () { console.log('我是一个方法') }

    // 这个只是创建了一个函数, 有一个函数空间
    // 但是这个函数并没有放在实例对象身上
    // 而且也会占用一部分空间, 每 new 一次, 就会创建一个
    // var fun = function () {}
}

// 本次调用的时候, 在本次的对象空间内有一个函数空间(xf0)存在
let p1 = new Person()
console.log(p1)

// 本次调用的时候, 在本次的对象空间内也有一个函数空间(xf1)存在
let p2 = new Person()
console.log(p2)

// xf1 和 xf0 两个函数空间里面是一模一样的代码
// 一模一样的函数, 但是占了两份内存空间, 有一份是浪费的
// 假设我 new 一百次, 就会有一百个对象空间
// 一百个对象空间里面既有一百个一模一样的函数, 那么其中 99 个是浪费的
console.log(p1.fn === p2.fn) // false 证明两个函数不是一个地址, 如果是一个地址就得到 true

在这里插入图片描述


prototype - 原型

  • 定义(背下来): 每一个函数都天生自带一个属性, 叫做 prototype, 它是一个对象
  • 构造函数也是函数, 所以构造函数也天生自带这个 prototype 属性
  • 当你想访问这个对象空间的时候, 就直接访问 函数名.prototype
function Person() {}

// 当你定义完毕函数, 这个空间就伴生存在了
// 就已经可以访问了, 他就是一个对象空间
// 和我们写的 {} 一样, 只不过是一个函数伴生的东西
console.log(Person.prototype)

// 既然是一个对象, 我就可以使用对象的操作语法, 在里面进行增删改查
// 添加个东西, 删除个东西, ...
// 向 Person 的原型上添加了一个叫做 fn 的成员, 值是一个函数
Person.prototype.fn = function () { console.log('我是一个函数') }
// 向 Person 的原型上添加了一个叫做 str 的成员, 值是一个字符串
Person.prototype.str = 'hello world'

console.log(Person.prototype)

原型方法

/*
	__proto__(原型属性)
        定义(背下来): 每一个对象天生自带一个属性, 叫做 __proto__, 指向所属构造函数的 prototype
        构造函数创造出来的对象, 也是对象
        所以也有 __proto__ 属性

        一个对象, 如果由 xxx 构造函数 new 出来的,
        那么我们就说这个对象所属的构造函数就是 xxx
        也就是说, new 出来的对象属于 xxx 构造函数
*/


function Person() {}

// 先向 prototype 里面添加一个成员
Person.prototype.fn = function () { cosnole.log('我是 Person 原型上的一个方法') }

// p1 是一个对象, 是由 Person 构造函数 new 出来的一个对象
// 所以 p1 就是属于 Person 这个构造函数的
// p1 所属的构造函数就是 Person
// 因为 p1 也是一个对象, 所以 p1 也有 __proto__ 属性
// p1 的 __proto__ 指向 所属构造函数(Person) 的 prototype
// p1.__proto__ === Person.prototype
let p1 = new Person()
console.log(p1)

// 因为 p1.__proto__ === Person.prototype 得到的是 true
// 说明这两个名字指向一个空间地址
console.log(p1.__proto__ === Person.prototype)

构造函数的合理化

  • 不合理
    • 当你的方法直接写在构造函数体内的时候, 会根据创建对象的多少
    • 有很多的方法是多余的, 占用更多没必要的资源
  • 合理化
    • 书写构造函数的时候

    • 属性全都写在构造函数体内

    • 方法全都写在构造函数的 prototype 上

    • function Person() {
          this.name = 'Jack'
      }
      Person.prototype.fn = function () {  }
      
  • 对象访问机制
    • 当你访问一个对象的成员的时候
    • 首先, 在自己身上查找, 有就直接使用, 停止查找
    • 如果自己没有, 会自动去 proto 上找
    • 如果有, 就直接使用, 停止查找
    • 如果没有, 就再去 proto 上查找(未完待续)
// 1. 准备一个构造函数
function Person() {
    this.name = 'Jack'
}

// 2. 向构造函数的 prototype 上添加一些成员
Person.prototype.fn = function () { console.log('我是 Person 原型上的一个方法') }
Person.prototype.age = 18
Person.prototype.name = 'Rose'

// 3. 使用 Person 构造函数创建一个对象
let p1 = new Person()
console.log(p1)
// 访问 p1 对象的 name 成员
// 因为 p1 自己就有, 所以就使用自己的
console.log(p1.name) // Jack
// 访问 p1 对象的 age 成员
// 因为 p1 自己没有, 所以就去自己的 __proto__ 上找
// 因为 p1.__proto__ === Person.prototype
// 所以就是去 Person.prototype 上找
console.log(p1.age)
// 访问 p1 对象的 fn 函数调用一下
// 因为 p1 自己没有, 所以就去自己的 __proto__ 上找
// 因为 p1.__proto__ === Person.prototype
// 所以就是去 Person.prototype 上找
p1.fn()

// 4. 使用 Person 构造函数再次创建一个对象
let p2 = new Person()

// 访问 p2 的 age 成员
// 因为自己没有, 就会去自己的 __proto__ 上找
// 因为 p2.__proto__ === Person.prototype
// 所以就是去 Person.prototype 上找
console.log(p2.age) // 18
// 访问 p2 对象的 fn 函数调用一下
// 因为自己没有, 就会去自己的 __proto__ 上找
// 因为 p2.__proto__ === Person.prototype
// 所以就是去 Person.prototype 上找
p2.fn()

// 此时, 我们构造函数的实例访问的方法
// 每一个实例身上都没有方法
// 需要访问的时候, 都是按照各个对象身上的 __proto__ 找到了
// Person.prototype 上面
// 每一个实例使用的方法, 都是 构造函数原型 上的那一个方法
// 避免了多次创造函数的问题, 只创建了一个函数
console.log(p1.fn === p2.fn) // true

在这里插入图片描述


和构造函数相关的 this

  • 构造函数体内的 this
    • 只要这个构造函数和 new 关键字连用了
    • 那么这个构造函数体内的 this 一定指向当前实例
    • 也就是本次调用的时候, 自动创建的那个对象
    • 也就是本次调用的时候, 你前面写的那个变量
  • 构造函数原型上的方法里面的 this
    • 看是怎么调用的 ?
    • 依靠实例调用
      • this 指向 当前实例
    • 直接使用原型调用 (基本不会这样使用)
      • this 指向 当前原型
function Person() {
    this.name = 'Jack'
}

// 我们之所以写在原型上, 就是为了给实例对象使用
// 因为直接写在构造函数体内, 会出现额外占用内存的问题
// 我们为了解决问题, 才把他写在 prototype 上
Person.prototype.fn = function () {
    console.log('我是 Person.prototype 上的 fn 方法')
    console.log(this)
}

// 使用构造函数
// 本次调用的时候, 因为和 new 关键字连用了
// Person 函数体内的 this 就指向 p1
let p1 = new Person()

// 我想调用那个 fn 方法, 我因该怎么书写
// 依靠实例调用
// 标准的对象调用方式 -> 点前面是谁就是谁
// 本次调用的时候, this -> p1, 当前实例
p1.fn()

// 直接使用原型调用
// 标准的对象调用方式 -> 点前面是谁就是谁
// 本次调用的时候, this -> Person.prototype
Person.prototype.fn()

constructor 构造器, 构造者

  • 每一个函数的天生自带的那个 prototype 里面都有一个这个属性
  • 指向的当前这个 prototype 所属的构造函数
  • xxx.prototype 里面的 constructor 就指向这个 xxx
  • 那一个构造函数的 prototype 里面的 constructor 就指向这个构造函数
当你访问任何一个对象的 constructor 的时候
  • 对象自己身上没有, 就会去 proto 中找
  • 实际上就是去 构造函数.prototype 中查找
  • 找到的 constructor 就是指向这个 构造函数
  • 换句话说, 每一个对象的 constructor 指向当前对象所属的构造函数
function Person() {}
console.log(Person.prototype) // {constructor: ƒ}
console.log(Person.prototype.constructor === Person) //true

function Student() {}
console.log(Student.prototype) // {constructor: ƒ}
console.log(Student.prototype.constructor === Student) //true
例子:万物皆对象
  • let arr = [ ]
    • 数组也是一个对象, 他是属于 Array 的
    • 当你访问 arr.constructor 的时候
    • 会去 arr.proto 中查找, 也就是去 Array.prototype 中查找
    • Array.prototype.constructor 就指向 Array
    • 所以 arr.constructor 就指向 Array
  • let fn = function () { }
    • 函数也是一个对象, 他是属于 Function 的
    • 当你访问 fn.constructor 的时候
    • 会去 fn.proto 中查找, 也就是去 Function.prototype 中查找
    • Function.prototype.constructor 就指向 Function
    • 所以 fn.constructor 就指向 Function
  • let time = new Date()
    • 时间对象也是一个对象, 他是属于 Date 的
  • let obj = { }
    • 对象也是一个对象, 他是属于 Object 的
var num = 123
var str = 'hello world'
var boo = true
function fn() {}
var reg = /^123$/
var time = new Date()
console.log(typeof num, num.constructor) // number ƒ Number() { [native code] }
console.log(typeof str, str.constructor) // string ƒ String() { [native code] }
console.log(typeof boo, boo.constructor) // boolean ƒ Boolean() { [native code] }
console.log(typeof {}, {}.constructor) // object ƒ Object() { [native code] }
console.log(typeof [], [].constructor) // object ƒ Array() { [native code] }
console.log(typeof fn, fn.constructor) // function ƒ Function() { [native code] }
console.log(typeof reg, reg.constructor) // object ƒ RegExp() { [native code] }
console.log(typeof time, time.constructor) // object ƒ Date() { [native code] }

if (num.constructor === Number) {
    console.log(' num 变量是一个数值类型的变量 ') // num 变量是一个数值类型的变量 
}
作用
  • 我们可以用它来判断数据类型
  • typeof 只能准确的判断基本数据类型, 不能准确的判断复杂数据类型
  • undefined 和 null 不能使用他来判断数据类型

准确判断所有数据类型

  • Object 是一个内置构造函数

  • 那么 Object 也有自己的原型, 叫做 Object.prototype

  • 在 Object 的原型上有一个方法叫做 toString()

  • 这个方法就可以检测所有的数据类型,但是需要和 call 连用

  • 语法: Object.prototype.toString.call(你要检测的数据类型)
  • 返回值: 是一个字符串

    • ‘[object 你检测的数据类型]’
console.log(Object.prototype.toString.call(123)) // [object Number]
console.log(Object.prototype.toString.call('hello world')) // [object String]
console.log(Object.prototype.toString.call(true)) // [object Boolean]
console.log(Object.prototype.toString.call(undefined)) // [object Undefined]
console.log(Object.prototype.toString.call(null)) // [object Null]
console.log(Object.prototype.toString.call({})) // [object Object]
console.log(Object.prototype.toString.call([])) // [object Array]
console.log(Object.prototype.toString.call(function () {})) // [object Function]
console.log(Object.prototype.toString.call(new Date())) // [object Date]
console.log(Object.prototype.toString.call(/abcd/)) // [object RegExp]

ES6 的语法 — 构造函数

  • 用 ES6 的语法书写构造函数,在 ES6 里面我们不叫作 构造函数 了, 叫做

  • 定义 类 的关键字是 class

  • 语法

  • class Person {
        constructor () {
            这个位置是 ES5 的构造函数体
            属性直接写在这里
            this.name = 'Jack'
        }
    
        // 这里开始的位置都是原型
        // 方法直接写在这里就好了
        // 不需要 function 关键字
        fn1 () {}
    }
    
  • 类 和 构造函数的区别

    • 构造函数本质上还是函数, 还有 function 关键字, 所以可以被当作普通函数调用,只不过没有了创造对象的能力, 但是不会报错

      • // 构造函数
        function Person() {
            // 构造函数体, 属性写在这里
            this.name = 'Jack'
        }
        
        // 原型, 方法写在原型上
        Person.prototype.fn1 = function () {}
        Person.prototype.fn2 = function () {}
        
        let p1 = new Person()
        console.log(p1) // Person {name: "Jack"}
        
        
        // 构造函数也是函数, 可以当作普通函数调用
        Person()
        
    • 类, 不是函数了, 根本没有 function 关键字, 所以不可以被当作普通函数调用,使用的时候必须和 new 关键字连用,如果没有 new 关键字就会报错了

      • // ES6 的类
        class Person {
            constructor (name) {
                // 构造函数体
                this.name = name
            }
        
            // 这里就是原型
            fn1 () { console.log('我是 Person 原型上的方法 fn1') }
        
            fn2 () { console.log('我是 Person 原型上的方法 fn2') }
        }
        
        let p1 = new Person('Jack')
        console.log(p1) // Person {name: "Jack"}
        
        // 类不是一个函数, 不能单独调用, 必须和 new 关键字连用
        Person() // 报错
        
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值