JS-part14.1-面向对象开发 / 创建对象的四种方式 / 构造函数的书写和使用

本文介绍了JavaScript中的面向对象开发,详细阐述了创建对象的四种方式:字面量创建、内置构造函数创建、工厂函数创建以及自定义构造函数创建,并重点讨论了构造函数的书写和使用,包括其合理性和`prototype`、`__proto__`以及对象访问机制。通过实例展示了如何解决构造函数中`this`指向问题,提供了面向对象实现选项卡切换的方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

了解面向对象开发

+ 是一个开发思想(你写代码的方式)
+ 面向过程
  => 在开发的过程中, 关注每一个 步骤 细节 顺序, 实现效果
+ 面向对象
  => 在开发过程中, 只关注有没有一个对象能帮我完成

   例子: 我今天要吃面条
     + 面向过程
       1. 和面 - 多少面粉 多少水
       2. 切面 - 多宽 多细
       3. 煮面 - 多长时间
       4. 拌面 - 多少酱 多少面
       5. 吃面 - 一口吃多少
     + 面向对象
       1. 找一个面馆
       2. 点一碗面
       3. 等着吃
     + 面向对象: 对面向过程的高度封装(高内聚低耦合)

在开发过程中
 + 面向过程
   => 按照顺序一步一步来
   
 + 面向对象(轮播图)
   => 直接找到一个对象, 能帮我完成轮播图
   => JS 本身没有, 我们需要第三方 swiper
   => swiper: 生成一个完成轮播图的对象
   
 + 我们:
   => 当你需要完成一个功能 A 的时候
   => 我们找到 JS 有没有这个完成功能 A 的对象
   => 如果没有, 我们制造一个"机器"
   => 这个"机器"可以制造完成功能 A

"机器" 是什么
 + 能力: 能创造一个 有属性 有方法 的 合理的 对象
  + 构造函数就是这个 "机器"

当我们完成一个功能的时候, 我们先利用构造函数来书写一个完成功能的"机器", 然后用构造函数去创建一个对象来帮助完成

模拟: 选项卡
  + 面向过程
    1. btns: [按钮1, 按钮2, 按钮3]
    2. tabs: [盒子1, 盒子2, 盒子3]
    3. 事件: 让 btns 里面的成员添加点击事件, 操作 btns 和 tabs 里面的每一个
  + 抽象成对象
    o = {
      btns: [按钮1, 按钮2, 按钮3]
      tabs: [盒子1, 盒子2, 盒子3]
      方法: function(){
        给 o.btns 里面的每一个绑定事件
        操作 o.btns 和 o.tabs 里面的每一个操作类名
      }
    }
  + 面向对象
    + 书写一个构造函数
    => 能创建一个对象包含三个成员
      1. btns
      2. tabs
      3. 方法, 能操作自己的 btns 和 tabs 的方法
    => 使用这个构造函数创建一个对象, 根据你传递的参数来实现选项卡效果

创建对象的四种方式

1. 字面量创建

var obj = {}
let o1 = {
    name: 'Jack',
    age: 18,
    gender: '男'
}

// 当我需要第二个对象的时候
let o2 = {
    name: 'Rose',
    age: 20,
    gender: '女'
}

对于Key值相同的对象无法批量创建, 不满足"高内聚低耦合"

2. 内置构造函数创建

var obj = new Object()
let o1 = new Object()
o1.name = 'Jack'
o1.age = 18

// 当我想创建第二个对象的时候
let o1 = new Object()
o1.name = 'Rose'
o1.age = 20

和字面量创建相同, 不能批量创建

3. 工厂函数创建对象

1. 先自己做一个工厂函数
2. 使用自己做的工厂函数来创建对象
// 1. 创建一个工厂函数
function createObj(name, age, gender){
    // 1-1. 手动创建一个对象
    let obj = {}

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

    // 1-3. 手动返回这个对象
    return obj
}

// 2. 使用工厂函数创建对象
// 创建第一个对象
let o1 = createObj('Jack', 18, '男')
console.log(o1)

// 创建第二个对象
let o2 = createObj('Rose', 20, '女')
console.log(o2)

4. 自定义构造函数创建

1. 自己书写一个构造函数
2. 使用构造函数创建对象

构造函数
  + 就是普通函数
  + 只有在你调用的时候和 new 关键字连用, 才有构造函数的能力
    => 只要你和 new 关键字连用, this => 当前对象(new 前面的变量名)
// 1. 创建一个构造函数
function createObj(name, age, gender){
    // 1-1. 自动创建一个对象

    // 1-2. 手动向对象上添加内容
    this.name = name
    this.age = age
    this.gender = gender

    // 1-3. 自动返回这个对象
}

// 2. 创建对象
let o1 = new createObj('Jack', 18, '男')  // 本次调用的时候, 函数内部的 this 就指向 o1
console.log(o1)

// let o2 = createObj('Rose', 20, '女')   // undefined, 因为没有关键字 new , 构造函数里不自动返回对象, 即没有创造对象的能力

let o2 = new createObj('Rose', 20, '女')
console.log(o2)

在这里插入图片描述

构造函数

+ 明确: 构造函数也是函数, 只不过是在调用的时候和 new 关键字连用了
+ 目的: 就是为了创建一个有属性 有方法 合理的 对象

有属性 有方法 合理 的 对象

构造函数的书写和使用

1. 调用必须有 new 关键字
  => 如果没有, 那么没有创建对象的能力
  => 只要有, 就会自动创建一个对象
  
2. 在构造函数内部不要写 return
  => 如果 return 基本数据类型, 写了也白写, 得到的还是对象
  => 如果 return 复杂数据类型, 构造函数白写, 返回复杂数据类型
  
3. 构造函数在调用的时候, 如果不需要传递参数, 最后的小括号可以不写
  => 但是最好都写上
        
4. 构建函数推荐首字母大写
  => 直观, 区别普通函数
  => 看到首字母大写的函数, 基本上就要和 new 连用
  
5. 当函数和 new 关键字连用
  => 会创造对象, 我们将创造出来的对象叫做 实例对象
  => 我们将创造的过程叫做 实例化 的过程
  => 构造函数体内的 this 指向当前实例对象
  => 也就是本次 new 的时候创建的那个对象
// 1. 必须和 new 关键字连用
function fn(){}
let o1 = new fn()
console.log(o1)
let o2 = fn()
console.log(o2)

在这里插入图片描述

// 2. 不要写 return
function fn(){
    this.a = 100

    // 基本数据类型写了白写
    // return 123
    // 复杂数据类型, 构造函数白写
    return new Date()
}

let o1 = new fn()
console.log(o1)

在这里插入图片描述

// 3. 不传递参数, ()可以不写
function fn(){}
let o1 = new fn()
console.log(o1)
let o2 = new fn
console.log(o2)

在这里插入图片描述

// 4. 首字母大写
function Fn(){
    this.a = 100
    this.b = 200
    this.c = 300
}

function fun(){
    var a = 100
    var b = 200
    return a + b
}

// 内置构造函数 Object Array Date
// Swiper Vue
// 5. 当函数和 new 关键字连用
function Person(name){
    this.name = name
    this.sayHi = function(){ console.log('hello world')}
}

// 本次 new 的时候, Person 会创造一个对象
// 把创造出来的对象赋值给了 变量p1
// 我们把 p1 叫做 Person 的实例化对象
// 本次调用的时候, Person 函数内部的 this 就指向 p1
let p1 = new Person('张三')

// 本次 new 的时候, Person 创造第二个对象
// 赋值给了 p2 变量
// p2 也是 Person 的实例化对象
// 本次调用的时候, Person 函数内部的 this 就指向 p2
let p2 = new Person('李四')

console.log(p1, p2)

p1.sayHi()
p2.sayHi()

在这里插入图片描述

构造函数是否合理

用下面的例子举例

function Person(name){
    this.name = name
    this.sayHi = function a(){ console.log('你好 世界')}
}

let p1 = new Person('张三')  // 本次创建的时候, 会开辟一段内存空间, 存储 a 函数
let p2 = new Person('李四')  // 本次创建的时候, 会开辟一段内存空间, 存储 a 函数
+ 一旦在构造函数体内书写方法的时候
+ 你创建多少个实例化对象, 那么就有多少个方法要占用内存空间
+ 不合理: 因为会有多余的函数内存空间被占用

在这里插入图片描述
同样的a函数却占用了两个内存空间

那思考下怎么解决?

/*思考1*/

// 函数单独提取出来没有问题
// 新问题: 但是每一个函数占用一个变量名
function a(){ console.log('你好 世界')}
function b(){ console.log('你好 世界')}

function Person(name){
    this.name = name
    this.sayHi = a
    this.sayHi2 = b
}

let p1 = new Person('张三')  
let p2 = new Person('李四')  

/*思考2*/

// 把所有的方法放在一个对象里面
// 新问题: 我要是有一百个构造函数, 就要有一百个对象
let PersonObj = {
    a: function a(){ console.log('你好 世界')},
    b: function b(){ console.log('你好 世界')}
}

function Person(name){
    this.name = name
    this.sayHi = PersonObj.a
    this.sayHi2 = PersonObj.b
}

let p1 = new Person('张三')  
let p2 = new Person('李四')

那就没完没了了

那又怎么解决呢?
我们要借助三个知识点 prototype 和 __ proto __ 和对象访问机制




prototype (原型 / 原型对象)

+ 定义: **每一个函数天生自带一个属性叫做 prototype, 他是一个对象**
+ 只要函数定义好以后, 这个 prototype 就出生了
+ 构造函数也是函数, 构造函数也有 prototype, 我们可以向里面添加一些内容
+ 这个天生自带的 prototype 里面有一个属性叫做 constructor
  => 表示我是哪一个构造函数伴生的原型对象
function Person(){}

// Person.prototype 已经出生了
// 向 prototype 添加一些些属性和方法
Person.prototype.sayHi = function(){ console.log('hello world')}

console.log(Person.prototype)

在这里插入图片描述

在这里插入图片描述

__ proto __

__proto__  (前后各俩下划线)
 + 定义: **每一个对象天生自带一个属性, 叫做__proto__, 指向所属构造函数的 prototype**
 + 实例化对象也是一个对象
 + 实例化对象也有 __proto__ 属性
function Person(){}

// Person 出生的时候就带着 prototype
//  向里面添加一些内容
Person.prototype.sayHi = function(){ console.log('hello world')}

// 因为 p1 是 Person 实例化出来的对象
// 所以 p1 所属的构造函数就是 Person
let p1 = new Person()
console.log(p1)

// 当我访问 p1.__proto__ 的时候就应该有 sayHi
console.log('Person.prototype', Person.prototype)
console.log('p1.__proto__', p1.__proto__)
console.log(Person.prototype === p1.__proto__)  // true

在这里插入图片描述

在这里插入图片描述
介绍完这俩知识点, 怎么将prototype 和 __ proto __ 运用到构造函数的合理性上呢?

对象访问机制 (上)

+ 当你访问一个对象的成员的时候
+ 如果对象自己本身有, 直接返回结果给你, 停止查询
+ 如果对象自己本身没有, 会自动去 __proto__ 上访问
+ 有就给你返回结果, 停止查询
+ 如果还没有(未完待续)

利用 prototype, __proto__ 和对象访问机制
  + 解决了构造函数的不合理
  + 属性直接写在构造函数体内
  + 方法书写在构造函数的 prototype 上

  + 使用构造函数创建一个 有属性 有方法 合理的 对象
  + prototype 的作用: 就是为了书写一些方法给该构造函数的实例对象使用的
    => 因为这个构造函数的每一个实例都可以访问这个构造函数的 prototype
function Person(name, age){
    this.name = name
    this.age = age
}

Person.prototype.sayHi = function(){ console.log('hello world')}

// 使用 Person 创建一个对象
let p1 = new Person('Jack', 18)
console.log(p1)

// 当我访问 p1.name 的时候, 自己就有
console.log(p1.name)
// 当我访问 p1.sayHi 的时候, 自己没有
// 就去 p1.__proto__ 上找, 因为 p1.__proto__ === Person.prototype
// 实际上就是去 Person.prototype 上找
// 找到  sayHi
p1.sayHi()
下图至此就画完了

在这里插入图片描述

当我访问 p1.name 的时候, 自己就有

在这里插入图片描述

访问 p1.sayHi 的时候, 自己没有, 就去 p1.__proto__ 上找, 因为 p1.__proto__ === Person.prototype,
实际上就是去 Person.prototype 上找 sayHi

在这里插入图片描述

结果

在这里插入图片描述

// 再次创建一个实例化对象
let p2 = new Person('Rose', 20)

// 当我访问 p2.sayHi 的时候
// 因为自己没有, 去到 p2.__proto__ 上找
// 实际上也是去 Person.prototype 上找
p2.sayHi()

在这里插入图片描述
在这里插入图片描述

由此可见, 调用同样的一个 sayHi 函数只占了一个函数内存空间
这样, 通过利用 prototype, __proto__ 和对象访问机制, 使我们的构造函数变得合理

构造函数相关的 this 指向

1. 构造函数体内的 this 指向
  => 因为和 new 关键字连用, this 指向当前实例
2. 构造函数原型上的方法里面的 this 指向
  => 因为方法是依靠实例对象在调用
  => this 指向当前实例
function Person(){
    this.name = 'Jack'
    console.log('构造函数体内 : ', this)
}

Person.prototype.fn = function(){ 
    console.log('我是Person.prototype 上的 fn 方法')
    console.log('Person.prototype.fn : ', this)
}

// 创建实例
let p1 = new Person()

console.log('===========')

// 调用方法
// fn 是 Person.prototype 上的方法
// 但是调用是依靠当前实例在调用
// 标准的对象调用方式
// p1.fn()  this 点前面是谁就是谁
p1.fn()   // this => 当前实例

在这里插入图片描述

案例: 面向对象的选项卡

1. 抽象内容
  + 属性 btns
  + 属性 tabs
  + 方法 能实现点击事件切换的方法

2. 书写构造函数
  + 接收一个参数
 
3. 在方法里面实现选项卡
  => 循环绑定事件以后, this 指向不再是实例了
  => 拿不到 btns 和 tabs
  => 解决方案1: 
    -> 提前保存 this 为一个变量
  => 解决方案2:
    -> 使用箭头函数
    -> 因为箭头函数没有 this, 就是外部作用域的 this
<div class="box">
    <ul>
        <li class="active">1</li>
        <li>2</li>
        <li>3</li>
    </ul>
    <ol>
        <li class="active">1</li>
        <li>2</li>
        <li>3</li>
    </ol>
</div>

解决方案1: 提前定义一个变量保存 this

Tabs.prototype.change = function(){
   // 这个位置的 this 是当前实例
   let _this = this
   // 从现在开始 _this 和 this 都是当前实例

   // 操作的是当前实例的 btns 和 tabs
   // this 就是当前实例, 我们就要给 this.btns 的每一个添加点击事件
   this.btns.forEach(function(item){
       item.addEventListener('click', function(){
           // 操作的还是当前实例的 btns 和 tabs 每一个的类名
           // 此时的 this 是谁: 事件源: 就是你现在点击的这个 li
           console.log(this)
           // 当你访问 _this 的时候, 访问我提前保存好的 _this
           // 也就是当前实例
           console.log('_this', _this)
       })
   })
}

解决方案2: 使用箭头函数

Tabs.prototype.change = function(){
    // 操作的是当前实例的 btns 和 tabs
    // this 就是当前实例, 我们就要给 this.btns 的每一个添加点击事件
    this.btns.forEach((item, index) => {
        item.addEventListener('click', () => {
            // 操作的还是当前实例的 btns 和 tabs 每一个的类名
            // 此时的 this 是谁(使用普通函数定义): 事件源: 就是你现在点击的这个 li
            // 此时的 this 是谁(使用匿名函数定义后): 因为使用匿名函数, this => 当前实例
            console.log(this)
        })
    })
}

最终代码

// ele 代表范围元素
function Tabs(ele){
    // 拿到出现选项卡的范围
    this.ele = document.querySelector(ele)
    // 找到 btns
    this.btns = this.ele.querySelectorAll('ul > li')
    // 找到 tabs
    this.tabs = this.ele.querySelectorAll('ol > li')

    // 这里的 this 就是当前实例
    // 用当前实例去调用 change 方法
    this.change()
}


Tabs.prototype.change = function(){
    // 操作的是当前实例的 btns 和 tabs
    // this 就是当前实例, 我们就要给 this.btns 的每一个添加点击事件
    this.btns.forEach((item, index) => {
        item.addEventListener('click', () => {
            // 操作的还是当前实例的 btns 和 tabs 每一个的类名
            // 此时的 this 是谁: 因为使用匿名函数, this => 当前实例

            // this.btns 和 this.tabs 里面的每一个都没有类名
            this.btns.forEach((t, i) => {
                t.className = ''
                this.tabs[i].className = ''
            })

            // 给对应的添加类名
            item.className = 'active'
            this.tabs[index].className = 'active'
        })
    })
}

new Tabs('.box')

同时实现多个选项卡
各个选项卡之间互不干扰, 想要样式不同, 可以写 css
在这里插入图片描述

new Tabs('.box')
new Tabs('.box2')

在这里插入图片描述
要是想像swiper一样, 有两个参数: 第一个参数: 容器类名, 第二个参数: 配置

以选项卡的切换方式为例:

function Tabs(ele, options = {}){
    this.ele = document.querySelector(ele)
    this.btns = this.ele.querySelectorAll('ul > li')
    this.tabs = this.ele.querySelectorAll('ol > li')

    // 初始化一下 options
    this.options = options

    // 用当前实例去调用 change 方法
    this.change()
}

Tabs.prototype.change = function(){
            this.btns.forEach((item, index) => {
            	// 调用时没有输入配置就默认 'click'
                item.addEventListener(this.options.type || 'click', () => {
                    this.btns.forEach((t, i) => {
                        t.className = ''
                        this.tabs[i].className = ''
                    })

                    item.className = 'active'
                    this.tabs[index].className = 'active'
                })
            })
        }

new Tabs('.box')
new Tabs('.box2', {type: 'mouseover'})

这时, 第一个选项卡是点击切换, 第二个选项卡是移入切换

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值