在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 模式的验证方法
-
isPrototypeOf, 用来判断 prototype 对象和某个实例之间的关系
Person.prototype.isPrototypeOf(person1) // true
-
hasOwnProperty 判断方法是自己独有属性,还是 prototype 的属性
person1.hasOwnProperty('name') // true
-
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 发现已经引用过的对象直接返回,保持这种引用关系