面向对象

510 阅读11分钟

首先,我们要明确,面向对象不是语法,是一个思想,是一种编程模式

面向对象介绍

  • OAA 面向对象分析

  • OOD 面向对象设计

  • OOP 面向对象编程

  • JavaScript 支持两种编程

    1. 面向过程 早期
    2. 面向对象 适合大的项目(多人团队)

    OOP面向对象编程的三个基本特征是:封装、继承、多态 1.封装 将相同的属性和方法,提取成为一个类,类是抽象 对象是类的实例(具体) 类是对象的模板(抽象) 2.继承 子类拥有父类的属性和方法 3.多态 1.重写 override 子类重写父类的属性和方法 2.重载 overload 在同一个类中,同名不同参 js是不支持重载,相同的方法名会被覆盖

构造函数

创建及实例化

  • 通过new关键字来调用
// 创建
function Person(name,age){
    this.name = name
    this.age = age
}
// 实例化
var p1=new Person("杨超越",25)

function Dog(name,age){
    this.name = name
    this.age = age
}
var d2 = new Dog("旺财",2)

console.log(p1) //Person {name: '杨超越', age: 25}
console.log(d2) //Dog {name: '旺财', age: 2}

注意点

  1. 首字母大写(约定俗成)

  2. 通过构造函数实例化对象,必须使用 new

  3. 构造函数不写return

  4. 构造函数能当成普通用?

    • 返回undefined
  5. this指向问题

    • 构造函数中的this指向实例化对象

原型

  • 原型:prototype 是函数中一个自带的属性, 我们创建的每个函数都有一个prototype(原型)属性, 这个属性是一个对象

  • 作用:可以让同一个构造函数创建的所有对象共享属性和方法

    • 也就是说, 可以不在构造函数中定义对象的属性和方法, 而是直接将这些信息添加到原型对象中
  • 优点 : 可以减少内存的使用

  • 原型模式的执行流程:

    1.先查找实例对象里的属性或方法,如果有,立刻返回;

    2.如果实例对象里没有,则去它的原型对象里找,如果有就返回;

image.png

image.png

写法

写法1(不推荐)

  • 属性和方法都写在原型上(不推荐)
function Person() { }    //声明一个构造函数
Person.prototype.name = "zhang"   //在原型里添加属性
Person.prototype.age = 100;
Person.prototype.show = function () {     //在原型里添加方法
    return this.name + this.age;
};
var person = new Person();
console.log(person)

// 比较一下原型内的方法地址是否一致
var person1 = new Person();
var person2 = new Person();
console.log(person1.show == person2.show);  //true,方法的引用地址一致

image.png

写法2(字面量)

  • 原型的字面量写法(推荐)
function Person(name, age, sex) {
    this.name = name
    this.age = age
    this.sex = sex
}
// 批量添加方法
//  原理就是对象覆盖
Person.prototype = {
    constructor: Person,  //强制指回构造函数
    eat() { },
    run() { },
    sleep() { }
}
// 单个添加方法
// Person.prototype.eat = function(){}
// Person.prototype.run = function(){}
// Person.prototype.sleep = function(){}
console.log(Person.prototype)
var p1 = new Person()
var p2 = new Person()
console.log(p1==p2) //false 地址不同
console.log(p1.prototype==p2.prototype) // true 原型相同

image.png

属性

  • constructor: 是原型的属性, 指向原型对象所属的构造函数;

  • __proto__: 是对象的属性,指向构造函数的原型

  • isPrototypeOf() : 判断一个对象是否指向了该构造函数的原型对象

  • hasOwnProperty() : 判断实例对象中是否存在该属性

    • 实例对象里有(私有)则为true,否则为false
  • in操作符: 判断属性是否存在于该实例对象或者该对象的原型中

    • 存在实例中或原型中(公有或私有都是true)
function Person() {
    // this.name = "杨超越"
}
Person.prototype.name = "虞书欣"
var p = new Person()
console.log(p.name)

//  constructor: 是原型的属性, 指向原型对象所属的构造函数
console.log(Person.prototype.constructor)

// __proto__: 是对象的属性,指向构造函数的原型
console.log(p.__proto__)
console.log(p.__proto__ === Person.prototype)
        
// isPrototypeOf(): 判断一个对象是否指向了该构造函数的原型对象
console.log(Person.prototype.isPrototypeOf(p));  //true, 实例对象都会指向

// hasOwnProperty():判断构造函数内部是否存在该属性
// 实例对象里有(私有)则为true,否则为false
console.log(p.hasOwnProperty("name"))

// in操作符:判断构造函数有没有该属性,内部和原型链都查找
// 存在实例中或原型中(公有或私有都是true)
console.log("name" in p)

// 封装一个方法判断是否为原型链上的属性和方法
function isPrototype(o, attr) {
    return attr in o && !o.hasOwnProperty(attr)
}
console.log(isPrototype(p, "name"))

image.png

内置对象的原型

console.log(Array.prototype.sort);            //sort 就是 Array 类型的原型方法
console.log(String.prototype.substring);  //substring 就是 String 类型的原型方法

// 使用原型可以给已有构造函数添加方法:
// 例如: 给String类型添加一个方法
String.prototype.addstring = function () {	
      return this + ',被添加了!';  //this 代表调用的字符串
};
console.log('张三'.addstring());    //使用这个方法 --> 张三,被添加了!

原型+构造函数

  • 单独使用原型来给对象添加属性和方法, 是有缺点的, 具体有以下两点 :(仅使用原型的缺点

    1. 它省略了构造函数传参初始化这一过程, 带来的缺点就是初始化的值都是一致的
    2. 原型对象共享的属性或者方法是公用的, 在一个对象(引用类型)修改后,会影响其他对象(引用类型)对该属性或方法的使用
  • 构造函数+原型模式

    • 使用构造函数添加私有属性和方法,

    • 使用原型添加共享的属性和方法

    • 优点:

      1. 实例对象都有自己的独有属性
      2. 同时共享了原型中的方法,最大限度的节省了内存
      3. 支持向构造函数传递参数 (初始值)

继承

继承的介绍

  • 概念:子类继承父类的属性和方法

  • 继承的特点:

    • 子类拥有父类的属性和方法;
    • 子类可以有自己新的属性和方法;
    • 子类可以重写(override)父类的方法 (js里面没有重载 overload,同名不同参)
  • 继承的优点

    • 代码复用:子类可以继承父类的属性和方法
    • 更灵活:子类可以追加或修改属性和方法

继承的方式

对象冒充

  • 构造函数继承
  • 使用bind、call、apply实现
  • 缺陷:不能继承原型上的属性和方法
// 父类
function Person(name,age,sex){
    this.name = name
    this.age = age
    this.sex = sex
}
Person.prototype.run = function(){
    console.log(`${this.name}喜欢跑步`)
}

// 子类
function Student(sId,name,age,sex){
    Person.call(this,name,age,sex)
    this.sId = sId
}
var p1 = new Person("大刘",50,"男")
var s1 = new Student("1001","小刘",20,"男")
console.log(p1)
console.log(s1)

image.png

原型继承

  • 每个对象都有proto,它会去上一层查找属性和方法,巧妙利用原型链实现继承
  • 好处:让子类拥有父类的属性和方法,
  • 缺陷:虽然可以继承父类的构造函数的属性,但是无法设置构造函数的初始值
// 父类
function Person(name,age,sex){
    this.name = name
    this.age = age
    this.sex = sex
}
Person.prototype.run = function(){
    console.log(`${this.name}喜欢跑步`)
}

// 子类
function Student(name,age){
    this.name = name
    this.age = age
}
Student.prototype =new Person()

var s1 = new Student("张三",20)
console.log(s1)

image.png

function Preson(name,age){
    this.name = name
    this.age = age
}
Preson.prototype.say = function(){
    console.log(this.name,"hello")
}

//继承构造函数Preson的原型
Student.prototype = new Person()	
//在继承的基础上增加新的方法
Student.prototype.printClassroom = function(){
    console.log(this.classroom)
}
//使用相同的原型名称可以覆盖构造函数Preson的原型
Student.prototype.say = function(){
    console.log("你好!")
}
//在原来Preson构造函数中增加内容
Student.prototype.say2 = function(){
    this.say()
    console.log("你好!")
}

组合继承

  • 组合继承=对象冒充 + 原型继承
  • 子类会继承父类的原型属性,但是构造函数内部的属性优先,
  • 缺点:导致原型的属性多余(内存开销)
// 父类
function Person(name, age, sex) {
    this.name = name
    this.age = age
    this.sex = sex
}
Person.prototype.run = function () {
    console.log(`${this.name}喜欢跑步`)
}

// 子类
function Student(sId,name,age,sex){
    Person.call(this,name,age,sex)
    this.sId = sId
}
Student.prototype = new Person
var s1 = new Student("1001","张三",20,"男")
console.log(s1)
s1.run()

image.png

寄生组合继承

  • 采用ES5的Object.create() 创建一个空的原型对象
// 父类
function Person(name, age, sex) {
    this.name = name
    this.age = age
    this.sex = sex
}
Person.prototype.run = function () {
    console.log(`${this.name}喜欢跑步`)
}

// 子类
function Student(sId, name, age, sex) {
    Person.call(this, name, age, sex)
    this.sId = sId
}
inherit(Person,Student)
var s1 = new Student("1001", "张三", 20, "男")
console.log(s1)
s1.run()

// 方法
function inherit(base,child){
    // 1.定义一个变量接收父类的原型
    const baseProto = base.prototype
    // 2.父类原型中的构造器的值 = 子类
    //  相当于清空了父类原型中构造器的值
    baseProto.construct = child
    // 3.赋值,子类原型 = 父类
    child.prototype = baseProto
}

image.png

ES6 class类继承

  • 关键字:extendssuper
// 父类
class Person {
    constructor(name, age, sex) {
        this.name = name
        this.age = age
        this.sex = sex
    }
    run() {
        console.log(`我${this.name},能run...`)
    }
}

// 子类
class Student extends Person {
    constructor(sId, sex, name, age) {
        super(name, age, sex)
        this.sId = sId
    }
    //重写父类的run方法
    run() {
        console.log(`我${this.name}不能run...`)
    }
}
var s1 = new Student("1001", "男", "张三", 20)
console.log(s1)
s1.run()

image.png

代理

Object.defineProperty

  • Object.defineProperty() 代理

    • 默认不能被修改,也不能被遍历
  • 语法:Object.defineProperty(对象,属性名称,options)

一、通过Object.defineProperty定义的属性默认是
    1.无法被删除
    2.无法被修改
    3.无法被遍历

二、Object.defineProperty的返回值与传入的对象是同一个地址
      可以配置以下属性:
    value: "刘德华",  //初始值
    configurable: true,   // true允许被删除
    writable: true,     // true允许被修改
    enumerable: true,    // true允许被遍历 

 三、提供get()和set()两个方法
    注:get和set方法不能和value和writable属性一起使用
var obj = {};
var o = Object.defineProperty(obj, "name", {
    value: "刘德华",  //初始值
    configurable: true,   // true允许被删除
    writable: true,     // true允许被修改
    enumerable: true,    // true允许被遍历  
})
// delete obj.name  // 删除
// obj.name = "周星驰"  // 修改
console.log(obj)
// 遍历
for (let key in obj) {
    console.log(key, obj[key])
}
console.log(obj)
console.log(o)
console.log(obj === o)    //true
// obj===o,所以可以写成
var obj1 = Object.defineProperty({}, "name", {
    value: "刘德华",  //初始值
    configurable: true,   // true允许被删除
    writable: true,     // true允许被修改
    enumerable: true,    // true允许被遍历  
}) 

用法

var person = {
    name: "周杰伦"
}
//设置一个默认值
person.age = "空"
// 问题:直接操作person,给person添加属性,容易出现值不准确
// 聘请一个代理  (秘书)
var proxy = Object.defineProperty({}, "age", {
    set(val) {
        if (val >= 18 && val <= 50) {
            person.age = val
        }
    },
    get() {
        // 加工
        return "年龄" + person.age + "岁"
    }
})
// 设置 触发set()
proxy.age= 18
console.log(person)
// 通过代理查看设置的年龄
// 获取==>查看  触发:get()
console.log(proxy.age) 

new Proxy

// 被代理对象
var obj = {}
// 代理对象
const proxy = new Proxy(obj, {
    get(target, property) {
        console.log("get被触发了")
        return target[property]
    },
    set(target, property, value) {
        if (property == "age" && value >= 18 && value <= 50) {
            target[property] = value
        } else {
            console.error("值不合法")
            target[property] = 0
        }
    }
})
// 操作代理对象
// proxy.age = 15  //set() 不合法
proxy.age = 18
console.log(proxy)
console.log(proxy.age)  //get()
// 查看被代理对象是否有值
console.log(obj)
console.log(obj.age)

拷贝

浅拷贝

  • 除了第一层地址不共享,第二层及以上有地址共享,就是浅拷贝

对象

  1. Object.assign()
  2. ...展开运算符
  3. lodash的 _.clone
var person = {
    name: "老刘",
    age: 50,
    child: {
        name: "小刘",
        age: 20
    }
}
// 1.Object.assign()
// var newPerson1 = Object.assign({},person)
// 2. ...展开运算符
// var newPerson1 = { ...person }
// 3.lodash的 ._clone
var newPerson1 = _.clone(person)
person.name = "老老老刘"
person.child.name = "小小小刘"
console.log(person)
console.log(newPerson1)
console.log(person === newPerson1)    //false
console.log(person.child === newPerson1.child)  //true

image.png

数组

  1. ...展开运算符
  2. [].concat()
  3. arr.slice(0)
  4. arr.filter(item => item)
  5. lodash的 _.clone
var arr = ["你好", ["11", "22"], { id: 1, name: "aa" }, { id: 2, name: "bb" }]
// var arr1 = [...arr]
// var arr1 = [].concat(arr)
// var arr1 = arr.slice(0)
// var arr1 = arr.filter(item => item)
var arr1 = _.clone(arr)
arr1[0] = "你你你"
arr1[1][0] = "aaaaaa"
console.log(arr)
console.log(arr1)
console.log(arr === arr1) //false
console.log(arr[1] === arr1[1])   //true

image.png

深拷贝

  • 所有的地址都不共享

  • 方法

    1. JSON.stringify()和JSON.parse()能够实现深克隆,但是会丢失方法
    2. 自己写
    3. 使用lodash的_.clonedeep()
// 1.JSON.stringify()和JSON.parse()能够实现深克隆,但是会丢失方法
// 2.自己写
//      如何判断引用类型
//          1.Object.prototype.toString.call(要判断的数据)
//          2.要判断的数据.constructor.name [注:不太准确,建议用第一种]
// 3.使用lodash的_.clonedeep()

// ------------------JSON.stringify()和JSON.parse()---------------
console.log("------------------JSON.stringify()和JSON.parse()---------------");
// 克隆对象
var person0 = {
    id: 1, name: "周杰伦",
    show() { },
    child: {
    	id: 11, name: "小周"
    }
}
var person1 = JSON.parse(JSON.stringify(person0))
person1.child.name = "xiaoxiao"
console.log(person0)
console.log(person1)

// 克隆数组
var arr = [11, [22, 33], 44,function(){console.log();}]
var arr1 = JSON.parse(JSON.stringify(arr))
arr[0] = "你好"
arr[1][0] = "aaaa"
console.log(arr)
console.log(arr1);

// ------------------自己写---------------
console.log("------------------自己写---------------");
// 封装一个方法(采用递归)
function deep(o) {
    let temp
    if (Object.prototype.toString.call(o).includes("Object")) {
    	temp = {}
    } else if (Object.prototype.toString.call(o).includes("Array")) {
    	temp = []
    }
    for (let key in o) {
        if (Object.prototype.hasOwnProperty.call(o, key)) {
            // 如果是引用类型,递归
            if (typeof o[key] == "object") {
            	temp[key] = deep(o[key])    //递归
            } else {
            	// 如果是值类型,直接赋值
           		temp[key] = o[key]
            }
        }
    }
    return temp
}

// 深克隆对象
var person0 = {
    id: 1, name: "周杰伦",
    show() { },
    child: {
    	id: 11, name: "小周"
    }
}
var person1 = deep(person0)
person0.child.name = "小小小周"
console.log(person0)
console.log(person1)

// 深克隆数组
var arr0 = [11, [22, 33], 44]
var arr1 = deep(arr0)
arr1[1][0] = "aaaaaaa"
console.log(arr0)
console.log(arr1)

// ------------------lodash的_.clonedeep()---------------
console.log("------------------lodash的_.clonedeep()---------------");
// 克隆对象
var person0 = {
    id: 1, name: "周杰伦",
    show() { },
    child: {
    	id: 11, name: "小周"
    }
}
var person1 = _.cloneDeep(person0)
person1.child.name = "小小小"
console.log(person0)
console.log(person1)

// 克隆数组
var arr = [11, [22, 33], 44]
var arr1 = _.cloneDeep(arr)
arr[1][0] = "AAAAA"
console.log(arr)
console.log(arr1)

闭包

  • 概念: 函数嵌套函数,内部函数可以引用外部函数的参数和变量,参数和变量不会被垃圾回收机制所收回

  • 形成闭包的3个条件

    1.函数嵌套函数

    2.利用作用域(全局/局部)

    3.GC(垃圾回收机制) 被使用就不会回收 (标记清除法,引用计数法)

  • 好处

    1. 缓存,在IE6、7、8下会存在内存溢出,谷歌可能会造成卡顿,所以要合理使用闭包
    2. 避免变量全局污染
  • 闭包的用途:

    1.实现缓存

    2.存储值与避免变量全局污染

    3.函数的柯里化

    4.节流和防抖

  • 垃圾回收机制:JS引擎会在一定的时间间隔来自动对内存进行回收(把内存释放)

    • JS垃圾回收机制有两种

      1. 标记清除: js会对变量做一个标记Yes or No的标签以供js引擎来处理, 当变量在某个环境下被使用则标记为yes, 当超出该环境(可以理解为超出作用域)则标记为no, js引擎会在一定时间间隔来进行扫描, 会对有no标签的变量进行释放(将该变量所占的内存释放掉)
      2. 引用计数: 对于js中引用类型的变量, 采用引用计数的内存回收机制, 当一个引用类型的变量赋值给另一个变量时, 引用计数会+1, 而当其中有一个变量不再等于值时, 引用计数会-1, 如果引用计数为0, 则js引擎会将其释放掉
  • 写法

//函数嵌套函数
function aa(){
  var a = 1;
  function bb(){
    console.log(a);
  }
  return bb;  //返回函数bb
}
//console.log(a);   //无法直接访问a
var cc = aa();       //aa函数被执行了, 并返回了bb给cc
console.log(cc)
cc();      //可以打印出a, 说明a并没有被释放