从零开始面向对象

140 阅读7分钟

面向对象

  • 含义:面向对象不是语法,是一个思想,一种编程模式

  • 面向过程:脸朝着过程 ---> 关注着过程的编程模式

  • 面向对象:脸朝着对象 ---> 关注着对象的编程模式

  • 实现一个效果

    • 在面向过程的时候, 我们要关注每一个元素, 每一个元素之间的关系, 顺序...
    • 在面向过程的时候, 我们要关注的就是找到一个对象来帮我做这个事情, 我等待结果

对象的创建方式

  1. 字面量创建对象
var obj = {}
obj.name = 'QF'
  1. 内置构造函数创建
var obj = new Object()
obj.name = 'QF'
  1. 使用工厂函数创建
// 1. 创建一个工厂函数
function createObj() {
    // 1.1 手动创建对象
    var obj = new Object()

    // 1.2 手动向对象中添加成员
    obj.name = 'QF'
    obj.age = 18

    // 1.3 手动返回一个对象
    return obj
}

// 2. 使用工厂函数创建对象
var o1 = createObj()
var o2 = createObj()
  1. 使用自定义构造函数
  • 自定义构造函数的函数名称首字母建议大写,区分普通函数
  • 自定义构造函数调用的时候必须和关键字 new 一起用
  • 自定义构造函数的小括号内也可以传参数
  • 自定义构造函数的this指向的不是window而是自动创建出的对象
  • 自定义构造函数不用写return
    • 返回基本数据类型没有效果
    • 返回引用数据类型就失去了构造函数的作用
// 1. 先书写一个构造函数
function Person(name, gender) {
    // 1.2 在构造函数内向对象添加一些成员
    this.age = 18
    this.name = name
    this.gender = gender
}
// 2. 使用这个构造函数创建一个对象 (和 new 连用)
var p1 = new Person('jack', 'man')
var p2 = new Person('rose', 'woman')
console.log(p1.age)  // 18
console.log(p2.age)  // rose

缺点

  • 自定义构造函数创建的对象内有函数的时候,每次调用都会创建一个内容完全一样新的函数,生成一个新的地址
    • 可以将函数创建在全局
    • 或者借助prototype

原型

  • 每个函数都有一个对象空间,构造函数也是函数
  • 将构造函数内声明的对象提取到一个公共的地方

prototype

  • 每个函数自带一个成员叫做 prototype,是一个对象空间
  • 这个对象空间可以通过函数名.prototype来访问
function Person () {}
console.log(Person.prototype)   // 是一个对象

Person.prototype是一个对象,那么也可以向对象中添加内容

function Person() {}
Person.prototype.name = 'prototype'
Person.prototype.sayHi = function () {}

重点:对象空间里存储的内容不是给函数用的,而是给构造函数的每一个实例化对象使用的

proto

  • 每一个对象都天生自带一个成员叫做__proto__
  • 实例化对象也是对象,所以也有__proto__
  • 这个 __proto__ 对象空间是给每一个对象使用的
  • 每个对象的__proto__ 都指向当前对象的构造函数的prototype,二者共有一个对象空间
function Person() {}
var p1 = new Person()
console.log(p1.__proto__ === Person.prototype)  // true
  • 将功能函数书写在构造函数的prototype对象空间内,调用的时候通过实例化对象来调用
  • 总结
    • 书写构造函数时
    • 属性直接写在函数体内部
    • 方法书写在构造函数的原型上

原型链

构造函数的prototype是一个对象,也有__proto__属性,那么这个对象的__proto__指向何处

  • 每一个对象都有所属的构造函数
    • 数组的构造函数是 Array()
    • 函数的构造函数是 Function()

constructor

  • 对象的__proto__里面有一个constructor属性,这个属性指向当前对象的构造函数

链状结构

  • 万物皆对象
    • js中所有引用数据类型都可以当作对象来使用
  • 对象内部属性的查找规则
    • 当我们在一个对象内部查找一个属性的时候, 会现在当前对象的自身查找
    • 如果找到直接使用并停止查找, 如果没有找到, 那么会去到当前对象的 proto 中继续查找
    • .......
    • 一直到最顶层的对象 Object.prototype, 还是没有找到, 那么返回一个 undefined

面试题

        /**
         *  1. p.__proto__                      === ???
         *      p 其实就是一个 实例化对象, 构造函数是 Person
         *          p.__proto__ === Person.prototype
         *
         *  2. Person.__proto__                 === ???
         *      Person 是构造函数, 本质上就是一个函数
         *      在 JS 中, 只要是一个函数, 那么就是 Function 这个构造函数的 实例化对象
         *          Person.__proto__ === Function.prototype
         *
         *  3. Person.prototype.__proto__       === ???
         *      Person 是一个构造函数, 本质上就是一个函数
         *      Person.prototype        其实就是一个函数的原型对象      本质上就是一个对象
         *      所以 Person.prototype.__proto__ 其实就是指向了一个对象的构造函数的原型对象
         *      因为JS 中 对象的构造函数就是 Object
         *      所以我们可以得到一个等式 Person.prototype.__proto__ === Object.prototype
         *
         *  4. Function.__proto__               === ???
         *      Function 是 JS 中内置的一个构造函数, 本质上就是一个函数
         *
         *          所以我们现在其实就是在找一个函数的构造函数的原型对象,   因为函数的原型对象是 Function
         *              Function.__proto__ === Function.prototype
         *
         *  5. Function.prototype.__proto__     === ???
         *      Function    内置构造函数, 其实就是一个函数
         *      Function.prototype  指向了当前构造函数的原型对象, 本质上就是一个对象
         *
         *          所以我们现在本质上就是在找一个对象的 构造函数的原型对象是谁
         *
         *              Function.prototype.__proto__ === Object.prototype
         *
         *  6. Object.__proto__                 === ???
         *      Object  是 JS 中 一个内置构造函数, 其实就是一个函数
         *          现在就相当于在找一个函数的 构造函数的原型对象
         *
         *              Object.__proto__ === Function.prototype
         *
         *  7. Object.prototype.__proto__       === ???
         *      在 JS 中最顶层的对象就是    Object.prototype, 所以如果再向上找的话, 就找不到任何东西了
         *      然后 在 JS 中, 给我们返回了一个数据 null
         *
         *          Object.prototype.__proto__ === null
        */

判断数据类型

  1. typeOf()
    • 缺点:引用数据类型判断不准确
  2. constructor
    • 数据.constructor === 构造函数;通过对比他等于那个构造函数, 然后确认他是那个数据类型
    • 缺点:undefined 和 null 不能用
    • constructor 是一个对象的内部属性,可以被修改
  3. instanceof
    • 数据 instanceof 构造函数
    • 缺点: undefined 和 null 不能使用
    • 引用数据类型有可能会被识别为对象类型
  4. Object.prototype.toString.call()
    • Object.prototype.toString.call(需要判断数据类型的数据)
    • 能判断js 中所有的数据类型

继承

定义:一个构造函数的实例使用另一个构造函数的属性和方法

  • 原型继承:用父类的实例化对象替换掉子类构造函数的原型对象
        function Person () {
            this.name = 'zhangsan'
        }
        Person.prototype.abc = function () {
            console.log('我是Person');
        }

        function Stu () {
            this.age = 20
        }
        
        Stu.prototype.asd = function () {
            console.log('我是Stu');
        }
        // console.log(new Stu());

        // 覆盖掉原来Stu.prototype里面的内容,如果想再Stu.prototype加内容需要在继承之后
        Stu.prototype = new Person()
  • 借用对象:在子类函数内调用父类函数并改变this指向(只能拿到对象本身的属性,原型空间内的不行)
        function Person () {
            this.name = 'zhangsan'
            this.sex = '男'
        }
        Person.prototype.abc = function () {
            console.log('我是Person');
        }

        function Stu () {
            this.age = 20
            // 再Stu里面调用Person函数
            // 把Person内的this改成Stu的实例化对象
            Person.call(this)
        }

        const stu = new Stu()
        console.log(stu);

        console.log(new Person());
  • 组合继承:原型继承和借用继承整合;但原型对象里面有一组多余的数据
function Person () {
            this.name = 'zhangsan'
            this.sex = '女'
        }
        Person.prototype.abc = function () {
            console.log('我是Person');
        }

        function Stu () {
            this.age = 20

            // 调用Person
            Person.call(this)

        }

        // 原型继承
        Stu.prototype = new Person()

        // 借用继承
        const s1 = new Stu()
        // 原型继承和借用继承的优点都有
        console.log(s1);
  • 拷贝继承:通过遍历父类实例化对象内的所有属性,添加到子类的构造函数原型对象中
        // 父类
        function Person(name, sex) {
            this.name = name
            this.sex = sex
        }
        Person.prototype.abc = function () {
            console.log('我是Person');
        }

        function Stu(age, ...args) {
            this.age = age
            // 实例化父类的构造函数
            const p = new Person(...args)

            // 通过遍历对象把父类的实例化对象添加到子类的protype
            // for...in 循环遍历会把对象的protype里面的键值对也遍历进去
            for (let key in p) {
                Stu.prototype[key] = p[key]
            }
        }

        const s1 = new Stu(18,'张三','男')
        console.log(s1);
  • ES6继承:固定写法
        // ES6 固定写法
        class Person {
            constructor(name, sex) {
                this.name = name
                this.sex = sex
            }
            abc() {
                console.log('我是Person');
            }
        }

        class Stu extends Person {
            constructor(age, ...args) {
                // super(父类的参数) ,必须写在constructor 的第一行
                super(...args)
                this.age = age
            }
        }
        const s1 = new Stu(18, '张三', '男')
        console.log(s1);

深浅拷贝

浅拷贝

  • for...in遍历对象
        const newObj = {}
        for (let key in obj) {
            newObj[key] = obj[key]
        }
  • 构造函数assign方法
// obj是想要拷贝的对象
const newObj = Object.assign({},obj)

深拷贝

  • 面试版
        const newObj = {}
        function fn(target, origin) {
            for (let key in origin) {
                const type = Object.prototype.toString.call(origin[key])
                if (type === '[object Object]') {
                    target[key] = {}
                    fn(target[key], origin[key])
                } else if (type === '[object Array]') {
                    target[key] = []
                    fn(target[key], origin[key])
                } else {
                    target[key] = origin[key]
                }
            }
        }
        fn(newObj, obj)
  • 工作版
// 通过JSON转字符串在转回对象
const newObj = JSON.parse(JSON.stringify(obj))