Javascript 封装、继承和深拷贝

143 阅读5分钟

在js没有 class 语法的时候,创建对象会怎么做呢,下面我们看下创建对象的方法

一.封装

1. 创建对象的原始模式

var Person = {
    name: '',
    age: '',
}

现在我们生成两个对象

var person1 = {}
person1.name = '1111'
var person2 = {}
person2.name = '222'

这样就生成了两个对象了,但是这样写起来复杂,然后方法属性不能复用

2.改进版,函数封装解决代码重复

function Person(name,age) {
    return {
        name,
        age
    }
}

现在我们生成两个对象

var person1 = Person('111')
var person2 = Person('222')

这种方法虽然做出了基本封装 但是两个对象之间没有内在联系,不能反映出他们是同一个原型对象的实例

3.构造函数模式

构造函数就是 一个普通函数,内部使用了 this 变量, 然后用 new 运算符,并生成实例,并且this 变量会绑定在实例对象上

function Person(name) {
    this.name = name
}

这样在生成两个对象

var person1 = new Person('1111')
var person2 = new Person('2222')

这样 两个对象内就含有一个 constructor 属性,指向他们的构造函数 Person

4.但是 3 这种方法同样存在内存浪费问题

如果在给 Person 添加更多的属性和方法,这样 new 的时候就会 多占一次内存

function Person(name, age) {
    this.name = name
    this.age = age
    this.eat = () => {}
    ......
}
var person1 = new Person(...)
var person2 = new Person(...)

// person1 person2 的通用的方法  eat 并不指向同一个地址
person1.eat == person2.eat   // false

所以能不能让eat 方法在内存中都指向一个地址,所以才有了  prototype 模式

5. prototype 模式

每一个 函数生成的时候,都自带一个 prototype 属性,也就是原型,指向原型对象,这个对象的所有属性和方法都会被构造函数继承,所以我们可以把公用的方法放到 prototype 上

function Person(name) {
    this.name = name
} 
Person.prototype.eat = () => {}

var person1 = new Person('1111')
var person2 = new Person('2222')

person1.eat == person2.eat // true

这样就提高了效率

6. prototype 模式的验证方法

  1. isPrototypeOf,  用来判断 prototype 对象和某个实例之间的关系

    Person.prototype.isPrototypeOf(person1) // true
    
  2. hasOwnProperty 判断方法是自己独有属性,还是 prototype 的属性

    person1.hasOwnProperty('name') // true
    
  3. in  判断某个实例是否含有某个属性, 不管是自己的还是  prototype 的

    'name' in person // true
    

二.继承:

子类可以使用父类的所有功能,并且对这些功能进行扩展。继承的过程,就是从一般到特殊的过程

1. call apply 继承

先创建一个 Parent 构造函数, 在 Child 函数中实现继承

function Parent() {
    this.attr1 = 111
}

function Child() {
    this.attr2 = 222
    Parent.call(this, arguments)
}

new Child().attr1 // 111

通过 绑定的方式将 父函数的方法继承到 子函数中

2. prototype 模式 继承

这种方法是通过重写, 将 一个实例对象赋值给构造函数的  prototype 来实现继承

function Parent() {
    this.attr1 = 111
}

function Child() {
    this.attr2 = 222
}

Child.prototype = new Parent()
new Child().attr1 // 111

但是这种方法就会导致 child 实例的 prototype 变成  Parent , 这种就不符合对象的结构,所以要手动修正 Child 的 prototype的constructor

Child.prototype = new Parent()
Child.prototype.constructor = Child

这样 Child 就继承了Parent的所有属性和方法

3. 直接继承 prototype 

2中的例子可以把  Parent 的属性 attr1 直接放到  Parent 的 prototype 上

function Parent() {}
Parent.prototype.attr1 = 111

然后  Child 直接继承 Parent.prototype

function Child() {
    this.attr2 = 222
}
Child.prototype = Parent.prototype
Child.prototype.constructor = Child

但是这种有一个问题就是,修改Child的 constructor, 也会修改掉Parent的  constructor ,由于指向指向同一个地址,所以这种方法还是不可取

4. 空对象中介 prototype 继承

由于3 中出现原型对象 指向同一导致父原型对象的 constructor 也被修改,我们可以找一个空对象作为中间媒介

var F = function() {}
F.prototype = Parent.prototype

Child.prototype = new F()
Child.prototype.constructor = Child

这样修改 Child 的prototype 不会影响到 Parent的prototype了,所以封装一下作为库使用,这也是常用的一种方法

 function extend(Child, Parent) {
    var F = function(){};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
 }

5. 拷贝继承

把父对象的所有属性和方法,拷贝进子对象

function extend(Child, Parent) {
    var p = Parent.prototype;
    var c = Child.prototype;
    for (var i in p) {
        if( p.hasOwnProperty(i)) {
            [i] = p[i]
        }
   }
}
// 还有一种方式是 assign
Object.assign({}, Parent)

不过这种是浅拷贝,深拷贝会导致引用是同一个地址,只是增加了一个指针,这种方式如果修改了子对象的引用型数据,父对象的也会改变

浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址, 

深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,深拷贝可以理解为 浅拷贝 + 递归

function cloneDeep(source) {
    var target = {}
    for( var i in source) {
        if(source.hasOwnProperty(i)) {
            if( typeof source(i) == 'object') {
                target[i] = cloneDeep(source[i])
            }else{
                target[i] = source(i)
            }
        }
    }
    return target
}

上面深拷贝的代码看上去已经很好了,但是还是存在很多问题

  • 没有参数校验

  • 判断是否为对象逻辑不够严谨

  • 没有考虑数组

    //增加辅助函数判断是否为对象 function isObject(obj){ return typeof obj == 'object' && obj != null } function cloneDeep(source) { if(!isObjet(source)) return source // 参数校验,如果不是对象直接返回 var target = Array.isArray(source) ? [] : {} // 数组兼容 for( var key in source ) { if(source.hasOwnProperty(key)) { if( isObject(source(key))) { target[key] = cloneDeep(source(key)) }else{ target[key] = source[key] } } } return target }

另外还有两个问题

  • 递归层级太深可能造成栈溢出
  • 循环引用也会造成栈溢出
  • 如果一个对象下的两个key 都引用同一个地址,深拷贝之后就会失去这种引用关系,变成两个不同对象

层级太深我们可以用一个辅助的栈来存储数据,然后循环栈,从而变量整个对象

循环引用,我们可以建一个 Map 发现已经引用过的对象直接返回,保持这种引用关系

具体可参考