Javscript中的对象

/post/js-proto article cover image

对象的原型

对象变量__proto__方法-和对象变量方法的不同

  • __proto__是每个构造函数实例自有的属性(非规范),指向构造函数的prototype
  • prototype是构造函数的显式属性又是实例化对象的原型,它自有的__proto__又指向Object.prototype

当在实例化之后修改构造函数的prototype时,实例化对象本身的__proto__还指向原来未修改的原型,此时的原型修改重新分配了内存地址而__proto__则保留了原来的地址引用

js
function Fn() {

}

Fn.prototype.something = 'a';

var instance = new Fn();

Fn.prototype = {
	something: 'b'
}

// 此时不想等
console.log(instance.__proto__.something, Fn.prototype.something)

<Callout>proto 是在查找链中用于解析方法等的实际对象,原型是当你用 new 创建一个对象时用来构建 proto 的对象,所谓"原型"</Callout>

对象的原型属性方法和对象的静态属性方法

js
function Fn() {
}

Fn.name = 'fn'; // 静态属性
Fn.excute = function() { // 静态方法
	console.log('excuted...')
}

Fn.prototype.akaNamme = 'foo'; // 原型属性
Fn.prototype.init = function() { // 原型方法
	console.log('inited...');
}

静态成员特点:

  • 外部访问静态成员,通过类名直接调用:<mark>Fn.excute()</mark>;
  • 静态方法访问其他属性成员: <mark>this.xxx</mark>
  • 静态方法和实例成员、对象成员之间互相不能访问
  • 一个静态方法改变了某个静态属性,其他静态方法或外部任何地方访问这个属性都会发生改变
  • 静态成员在实例对象创建前就已经分配内存空间且在之后的访问中也同样指向同一地址,直到内存销毁

<Callout> 无论是否创建对象,创建多少个对象,是否调用该静态方法或静态属性,都会为这个静态成员分配内存空间,一旦为静态方法或静态属分配好空间,就一直保存在内存中,直到被内存回收 </Callout>

静态成员适合的使用场景:

  • 当外部不能创建对象,就只能借助类内部的静态方法来获取类的对象;这时肯定不能把这个方法定义成原型对象属性上的方法,只能定义为类的静态方法,因为如果定义成原型对象属性的方法,就会导致外部无法被访问,因为外部根本不能创建对象,也就无法访问原型对象属性上的方法。而静态方法要访问的属性就只能是静态属性了,这也是静态属性的应用时机。
  • 当类中某个方法没有任何必要使用任何对象属性时,而且使用了对象属性反而让这个方法的逻辑不正确,那既如此,就应该禁止这个方法访问任何对象属性和其他的对象方法,这时就应该把这个方法定义为静态方法
  • 当一个类中某个方法只有一个或者 1-2个 对象属性,而且更重要的是,你创建这个类的对象毫无意义,我们只需要使用这个类的一个或者多方法就可以了,那么这个方法就应该定义为静态方法。常见的工具类中的方法通常都应该定义为静态方法。

静态成员不适合的使用场景:

  • 需要根据当前类产出新的对象且和内部属性状态有关联的场景。例如订单生成...

原型的继承

原型链继承

原型链继承实现的本质是改变Son构造函数的原型对象变量的指向【 就是Suber.prototype的指向 】,Suber.prototype= new Parent ( )。那么 Suber.prototype 可以访问 Super 对象空间的属性和方法。所以顺着__proto__属性 ,子类也可以访问 父类 的原型对象空间中的所有属性和方法。

js
function Super(name, type) {
	this.name = name;
	this.type = type;
}

function Suber(clor, size) {
	this.color = color;
	this.size = size;
}

Suber.prototype = new Super('sub', 'md1');

const subInstance = new Suber()

<Callout type="info"> 原型链继承查找属性和方法的完整路线描述: 子对象首先在自己的对象空间中查找要访问的属性或方法,如果找到,就输出,如果没有找到,就沿着子对象中的__proto__属性指向的原型对象空间中去查找有没有这个属性或方法,如果找到,就输出,如果没有找到,继续沿着原型对象空间中的__proto__查找上一级原型对象空间中的属性或方法,直到找到Object.prototype原型对象属性指向的原型对象空间为止,如果再找不到,就输出null </Callout>

<Callout> 如果需要把子类的原型使用一个对象完整赋值时,因为此时原型被重写内存地址更换所以需要显式的指定它的constructor指向,以保证原来的构造器指向不缺失。 </Callout>

js
Suber.prototype = {
	commonField: 'xxx',
	constructor: Suber
}

如果继承只是为了子类能够访问到父类的属性方法,那么使 Sub.prototype = Sup.prototype 会怎么样呢?

<Callout type="error"> 因为子类和父类的原型指向同一内存地址,所以每当子类的原型属性方法发生了改变也会导致父类的发生变化。使得继承关系变得混乱 </Callout>

原型链继承的不足: <u>不能通过子类构造函数向父类构造函数传递参数。</u>

借用构造函数继承

在子类内部通过apply()或者call()的方式调用并传递参数给父类

js
  function Parent (name, age) {
    this.name = name
    this.age = age
    console.log("this:", this)
    console.log("this.name:", this.name)
  }
  Parent.prototype.friends = ["xiaozhang", "xiaoli"]
  Parent.prototype.eat = function () {
    console.log(this.name + " 吃饭");
  }
  function Son (name, age, favor, sex) {
    this.favor = favor // 兴趣爱好
    this.sex = sex
    Parent.call(this, name, age)// TS继承中使用super
  }
  let sonobj2 = new Son("lisi", 34, "打篮球", "");
  console.log("sonobj2:", sonobj2)
  console.log("sonobj2.friends:", sonobj2.friends);//undefined

借用构造函数继承的不足: <u>借用构造函数实现了子类构造函数向父类构造函数传递参数,但没有继承父类原型的属性和方法,无法访问父类原型上的属性和方法。</u>

借用构造函数原型链继承组合模式

在具备原型链继承优点(实例对象可以访问到原型的属性和方法)的同时,使用借用构造函数弥补了其不能传递参数给父类的缺点

js
function People(name,sex,phone){// People父构造函数【看成是一个父类】//=Parent
			this.name=name; // 实例属性
        	this.sex=sex;
			this.phone=phone
	}
	People.prototype.doEat=function(){
        console.log(this.name + "吃饭...")
    }
	function ChinesePeople(name,sex,phone,national){ //=SON
		People.apply(this,[name,sex,phone]);// 借用父构造函数继承
		this.national=national;// 民族
	}
	ChinesePeople.prototype=new People("wangwu",'',"1111");

借用构造函数+原型链继承组合模式的缺点: <u>调用了两次父类构造函数(一次在是创建子类原型时调用,另一次是在子类构造函数中调用),多次的内存分配导致效率下降代码冗余</u>

原型式继承

通过调用此函数会创建一个临时构造函数,将传入的对象赋值给这个构造函数的原型,然后返回这个临时类型的一个实例。本质上是对传入的对象执行了一次浅复制

是Object.create的实现

js
function object (o) {
	function F() {}
	F.prototype = o;
	return new F();
}

let person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
let yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends);  // "Shelby,Court,Van,Rob,Barbie"

原型继承的缺点: <u>使用场景的局限性,只适合不需要单独创建构造函数,但仍需要在对象间共享信息的场合。且引用属性仍然是共享的</u>

寄生式继承

创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象

js
function createAnother(original){
  let clone = Object.create(original);  // 通过调用函数创建一个新对象
  clone.sayHi = function() {     // 以某种方式增强这个对象
    console.log("hi");
  };
  return clone;           // 返回这个对象
}

let person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = createAnother(person);
anotherPerson.sayHi();  // "hi"

寄生式继承的缺点: <u>通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。</u>

寄生式组合继承

通过借用构造函数继承属性,但使用混合式原型链继承方法。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。

js
function inheritPrototype(subType, superType) {
  let prototype = Object.create(superType.prototype);  // 创建对象
  prototype.constructor = subType;              // 增强对象
  subType.prototype = prototype;                // 赋值对象
}

function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
  console.log(this.name);
};
function SubType(name, age) {
  SuperType.call(this, name);
	this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {                                                2
  console.log(this.age);
};

<Callout type="info"> 这里只调用了一次 SuperType 构造函数,避免了 SubType.prototype 上不必要也用不到的属性, 因此可以说这个例子的效率更高。而且,原型链仍然保持不变,因此 instanceof 操作符和isPrototypeOf()方法正常有效。寄生式组合继承可以算是引用类型继承的最佳模式。 </Callout>

objectsetprototypeof和objectcreate的区别

虽然两者都可以为对象创建原型链接,但是Object.create设置原型(使用第二个参数)之后会把子类原型覆盖掉导致内存地址发生改变

js
function People (name, sex, phone) {
  this.name = name;
  this.sex = sex;
  this.phone = phone;
}

function Man(name, sex, phone) {
	People.call(this, name, sex, phone)
}

Man.prototype.show = function() {
	console.log('show');
}

function _extends (parent) {//继承
  let middle = { count: 23 }
  return Object.setPrototypeOf(middle, parent.prototype)
}

function _extends2(parent) {
	return Object.create(parent.prototype, {
    count: {
      writable: true,
      value: 23
    }
  })
}

const middle = _extends(People);
const middle2 = _extends2(People);

// Man.prototype = middle // can invoke prototype.show
// Man.prototype = middle2 // can not find prototype.show
Man.prototype.constructor = Man

<Callout type="info"> 前者是通过 proto = prototype 链接原型的方式,后者则是基于指定对象原型合并属性后创建新对象 </Callout>

静态属性的继承

js
for (let key in People) {//自有属性 还会查找__proto__指向的对象空间【这里是rootClass函数对象空间】中自有属性
   if (Object.prototype.hasOwnProperty.call(People, key)) {//要求返回true的条件是本构造函数的自有属性 不会查找__proto__指向的对象空间【这里是rootClass函数对象空间】中自有属性
  //console.log("key:", key);//静态属性和静态方法
  ChinesePeople[key] = People[key]//子类ChinesePeople继承父类People的静态属性和静态方法
  }
}
js
Object.keys(People).forEach((key) => {
  ChinesePeople[key] = People[key]
})
js
ChinesePeople.__proto__ = People
js
// 本质也是将前者的__proto__指向后者
Object.setPrototypeOf(ChinesePeople, People)