Javascript 对象深度解析

对象-高程及月影js视频学习笔记

对象的深拷贝和浅拷贝

ES5 浅拷贝

Object.assign({}, conf)

只能拷贝一级,深层的源改变,目标也会跟着改变。

递归 深拷贝

function deepCopy(des, src) {
  for (var key in src) {
    if(typeof src[key] !== 'object') {
      des[key] = src[key];
    } else {
      des[key] = des[key] || {};
      deepCopy(des[key], src[key]);
    }
  }
  return des;
}

创建对象

构造函数模式

new 和 Object.create

首先了解下Object.create的实现方式

Object.create =  function (o) {
    var F = function () {};
    F.prototype = o;
    return new F();
};

new操作符会做一下几个事情:

  • 创建一个新对象
  • 将构造函数的作用域赋给新对象
  • 执行构造函数中的代码(为新创建的对象添加属性)
  • 返回新对象

其中被new的函数就叫构造函数。

注意:

function C() {
  this.z = 3;
  this.func = function() {console.log(1)}
}
var c1 = new C();
var c2 = new C();
c1.func === c2.func // false

执行c1.func === c2.func会返回false,这是因为每次实例化的时候都会创造一个新的函数对象,实际上没有必要这么做,首先能想到的是,把要创建的函数放到构造函数外面:

function Person(name, age, job){this.sayName = sayName}
function sayName() {console.log(1)}

var p1 = new Person(1,2,3);
var p2 = new Person(1,2,3);

p1.sayName === p2.sayName // true

但是放在全局中就是去了封装的意义,所以引入了原型的模式。

原型模式

每个函数都有一个prototype(原型)的属性,这个属性指向一个对象,可以存储有特定类型的所有实例共享的属性和方法。

注:箭头函数不包含这个属性。。。

此时,下式依旧成立

function Person() {}
Person.prototype.name = 'aaa';
Person.prototype.sayName = ()=>{console.log(2)};

var p1 = new Person(1,2,3);
var p2 = new Person(1,2,3);

p1.sayName === p2.sayName // true

构造函数与原型对象

Person.prototype就是Person的原型对象,默认情况下,每个函数都存在prototype属性,这个属性存的对象里都默认有一个constructor属性指向该函数(当然还有一个_ proto_继承自Object)。

Person.prototype.constructor 指向 Person。

// _ proto_:显示在具体实例上的一个属性

// prototype:构造函数上的一个属性

isPrototypeOf

A.prototype.isPrototypeOf(B):A是不是B的原型对象

B instanceof A:B是不是A的实例

判断某个对象的[[Prototype]](实例,拥有[[Prototype]]属性的实例对象,通常浏览器的实现是_ proto_)是否指向调用isPrototypeOf()这个方法的对象(原型对象Person.prototype)。

读取某个对象的属性时,会按照实例对象、_ proto_ 对象、 _ proto__ proto_等等顺序依次查找,比如上面person的例子,可以找到p1和p2的sayName和name属性。

但是如果直接给p1.name赋值,则无法改变 _ proto_对象中的name值,会在对象实例本身添加这个属性,以后读取p1.name值时,因先从实例找起,所以不会再读到原型上的值。

Person
name: "bbb"
__proto__:{name:"aaa", sayName: ......}

但是通过修改_ proto_ 的name属性,就会同时改变p1和p2的原型的name值 以及 Person的原型对象的name值,因为Person.prototype === p1. _ proto_ ,Person.prototype === p2. _ proto_

使用delete操作符删掉p1.name,则又能访问到原型上的name属性,注意 delete p1.name只能删掉实例上的属性,可以通过delete p1.proto.name删掉原型上的属性,同样的操作原型的话,上述三个都会改变。

hasOwnProperty

可以通过这个方法来判断某属性是来自实例还是来自原型。

p1.hasOwnProperty('name') // false
in

可以判断是否对象实例或原型中有该属性。可以访问到不可枚举的属性,如constructor和_ proto_、prototype。也可以通过in和hasOwnProperty来判断属性是否在原型中。

for-in

只能访问到可枚举实例和原型属性,constructor和_ proto_、prototype等不可枚举的访问不到。

Object.keys(obj)

对象上所有可枚举实例属性。返回的是字符串数组

Object.getOwnPropertyNames()

对象上所有实例属性。返回的是字符串数组

原型的动态性

一般的,想之前提到的一样,每次修改原型对象是,能立即在所有对象实例中反应出来,因为他们之间的连接是一个指针,而给一个副本。

但是,重写原型对象的话,把原型修改为另一个对象,就切断构造函数与最初原型之间的联系。

重写之前的实例中的[[Prototype]]指针仍指向最初的原型。

在重写原型对象之后创建的实例[[Prototype]]指针指向重写的原型。z

// 重写原型对象
function Person() {}

var friend = new Person();

Person.prototype = {
  constructor: Person, // 如果不加这个的话,重写这里会没有constructor,prototype的prototype里会有Object的constructor。但是这种写法也有一个问题,constructor会变成可枚举的,所以也可以向下面那种方式写。
  name: 'aaa'
}
// Object.defineProperty(Person.prototype, 'constructor', {
  // enumerable: false, 
  // value: Person
// });
friend.sayName(); // error

原生对象的原型

原生对象比如Object,Array,String等,比如Array.prototype中有sort()方法。

根据动态性,可以给原生对象的原型上添加方法,比如Array,这样当前环境的所有数组都可以调用到新添加的方法。

PS: 不推荐在产品化的程序中修改原生对象的原型,这样可能会导致明明冲突,而且,也可能会意外的重写原生方法。

如果往数组上添加新方法,因为默认可枚举,所以for-in操作数组时,会把往数组上添加的新特性一起for-in出来。

解决这个问题,可以使用Object.defineProperty来给原型对象添加方法。使用此方法时,默认不可枚举。

原型模式创建对象的优缺点

优点

可以让所有对象实例共享它所包含的属性和方法(直接定义在构造函数中的函数,每个实例都会创造一个新的函数)

缺点
  • 只用原型模式,没有构造函数传递初始化参数这一环节,会导致默认情况下都取得相同的属性值。
  • 由于原型中的属性时被所有实例共享的,方法(function)比较适合这种共享的模式,其余的,改变一个实例的属性时,如果是值类型的,会在实例上创建这个属性,覆盖掉原型中的属性,不会影响到其他的实例;但是如果是引用类型的,比如下面这个例子,就会导致所有实例的该属性都改变,因为此时相当于直接在操作原型,并没有在实例上添加这个属性。
function Person() {}

Person.prototype = {
  constructor: Person,
  name: 'aaa',
  friends: [1, 2, 3]
}

var friend1 = new Person();
var friend2 = new Person();

friend1.friends.push(4);

console.log(friend1.friends) // [1, 2, 3, 4]
console.log(friend2.friends) // [1, 2, 3, 4]

组合使用构造函数模式和原型模式

在构造函数中定义各自特有的属性,在prototype中写共享的方法。

通常都是用这种方式创建自定义类型。

动态原型模式

在构造函数中初始化原型(仅在必要的情况下)

可以通过检查某个应该存在的方法是否有效,来决定是否要初始化原型。

function Person(name) {
  this.name = name;
  // 方法
  if(typeof this.name !== "function") {
    Person.prototype.sayName = function(){console.log(1)}
  }
}

但是只要有一次创建实例时,typeof this.name !== “function”,那么所有实例(之前创建和之后)都会有sayName这个方法。所以不能使用对象字面量来重写原型,这样就会切断之前创建的实例与新原型之间的联系。

寄生构造函数模式

封装创建对象的代码,再返回新创建的对象。

构造函数在不返回值的情况下,默认会返回新对象的实例(new操作符做的事情),寄生模式相当于手动做new操作符的一些事情,可以重写new调用构造函数时返回的值。

function Person(name) {
  var o = new Object();

  o.name = name;

  return o;
}

这种方式有个问题,因为相当于改变了new的默认行为,所以不存在默认的原型,原型就是Object,所以不能使用instanceOf来确定对象类型,所以一般情况下不要使用这种模式。

继承(原型链)

继承包括接口继承和实现继承。

实现继承通过原型链实现,利用原型让一个引用类型继承另一个引用类型的属性和方法。

…prototype = new Object()的方式才能继承,让一个原型对象等于另一个类型的实例。

直接…prototype = …prototype只是一直在修改原型,而不是链式继承。

这种方式有个弊端,在给对象原型赋值的时候,实例化了另一个类型(eg:A.prototype = new B(); ),即调用了类型B的构造函数,通常我们希望在实例化A的时候,再调用B的构造函数,如果B的构造函数有一些方法,或者需要传参的方法,这种传undefined参数的实例化可能会引发一些问题,可以用Object.create()来解决:

A.prototype = Object.create(B.prototype);

前文中提到过,Object.create是创建了一个构造函数为空新对象,赋原型在实例化,所以可以避免可能的构造函数异常执行。

原型链的问题

  • 之前原型模式创建对象的缺点中提到过,包含引用类性值的原型造成的问题。(不单独使用原型链,放到构造函数中)
  • 创建子类型实例时,不能向父类型(子类型继承父类型)中传参。(ES6中可以使用super([arguments]),比如react中的super(props),还有其他的解决办法,见下面的借用构造函数)

借用构造函数

使用apply()、call()等方法在新创建的对象上执行继承对象的构造函数。还可以通过此种方式绑定当前this并传参,或者用super。

getter和setter

使用Object.defineProperty()定义属性时,可以设置get、set、enumerable。

数据绑定视图

猜你喜欢

转载自blog.csdn.net/katecatecake/article/details/79368654