面向对象:构造函数和原型链

37 阅读8分钟

面向对象

segmentfault.com/a/119000001…

背景

(1)对象是单个实物的抽象。

一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个远程服务器连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。

(2)对象是一个容器,封装了属性(property)和方法(method)。

属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal对象,使用“属性”记录具体是哪一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。

JavaScript 语言使用构造函数(constructor)作为对象的模板。所谓”构造函数”,就是专门用来生成实例对象的函数。它就是对象的模板,描述实例对象的基本结构。一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。

特征

  • 封装

    将一个事物所有的状态(属性),行为(方法)封装成一个对象

  • 多态

    封装的对象生成不同的单个对象

  • 继承

直接创建对象

 var obj = new Object();
 //或
 var obj = {};
 //为对象添加方法,属性
 var person = {};
 person.name = "TOM";
 person.getName = function() {
     return this.name;
 }
 ​
 // 也可以这样
 var person = {
     name: "TOM",
     getName: function() {
         return this.name;
     }
 }

这种方式创建对象简单,但也存在一些问题:创建出来的对象无法实现对象的重复利用,并且没有一种固定的约束,操作起来可能会出现这样或者那样意想不到的问题。如下面这种情况。

 var a = new Object();
 var b = new Object();
 var c = new Object();
 c[a] = a;
 c[b] = b;
 console.log(c[a], a); //{} {}
 console.log(c[a] === a); //输出什么 false

工厂模式

 var createPerson = function (name, age) {
   // 声明一个中间对象,该对象就是工厂模式的模子
   var o = new Object();
   // 依次添加我们需要的属性与方法
   o.name = name;
   o.age = age;
   o.getName = function () {
     return this.name;
   };
   return o;
 };
 ​
 // 创建两个实例
 var perTom = createPerson("TOM", 20);
 var PerJake = createPerson("Jake", 22);
 console.log(perTom instanceof Object); //true
 console.log(perTom instanceof createPerson); //false
 console.log(perTom.__proto__, createPerson.prototype);//{} createPerson {} 实例的原型和构造函数的原型不一样

缺点:1.无法识别对象类型; 2.每个对象都有自己的 sayName 函数,函数不能共享,造成内存浪费

构造函数

 const p1 = {
   name: "foo",
 };
 function People(name) {
   console.log(this); //{ name: 'foo' } People {}
   this.name = name;
   console.log(this); //{ name: 1 } People { name: 'boo' }
 }
 ​
 const Foo = People.bind(p1); //改变this指向,将Foo作为构造函数
 Foo(1); //更改绑定的p1.name
 console.log(p1); //{ name: 1 }
 const foo = new Foo("boo");
 console.log(foo.name); // boo
 console.log(p1); //{ name: 1 }

构造函数模式和工厂模式存在一下不同之处

  • 没有显示的创建对象(new Object() 或者 var a = {})
  • 直接将属性和方法赋给this对象
  • 没有return语句
  • 函数共享
原型链

img

①所有引用类型都有一个__proto__(隐式原型)属性,属性值是一个普通的对象 ②所有函数都有一个prototype(原型)属性,属性值是一个普通的对象 ③所有引用类型的__proto__属性指向构造函数的prototype

 var a = [1,2,3];
 console.log(a.__proto__ === Array.prototype;) // true

所有对象都有自己的原型对象(prototype)。原型对象的所有属性和方法,都能被实例对象共享。当我们访问对象的属性或者方法时,会优先访问实例对象自身的属性和方法。

当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会去它的__proto__隐式原型上查找,即它的构造函数的prototype,如果还没有找到就会再在构造函数的prototype__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链

如果一层层地上溯,所有对象的原型最终都可以上溯到Object.prototype,即Object构造函数的prototype属性。也就是说,所有对象都继承了Object.prototype的属性。这就是所有对象都有valueOftoString方法的原因,因为这是从Object.prototype继承的。

这里写图片描述

这里写图片描述

Object.prototype的原型是nullnull没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null

 console.log(Object.getPrototypeOf(Object.prototype));// null
 console.log(Object.prototype.__proto__ === null);
new 命令的机制
 // 先一本正经的创建一个构造函数,其实该函数与普通函数并无区别
 const Person = function (name, age) {
   this.name = name;
   this.age = age;
   this.getName = function () {
     return this.name;
   }
 }
 // 将构造函数以参数形式传入
 function New(func) {
   // 声明一个中间对象,该对象为最终返回的实例
   const res = {};
   if (func.prototype !== null) {
     // 将实例的原型指向构造函数的原型
     res.__proto__ = func.prototype;
   }
   console.log(arguments,);
   // ret为构造函数执行的结果,这里通过apply,将构造函数内部的this指向修改为指向实例对象res
   const ret = func.apply(res, Array.prototype.slice.call(arguments, 1));
   // 当我们在构造函数中明确指定了返回对象时,那么new的执行结果就是该返回对象(即在构造函数中明确写了return this;)
   if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
     return ret;
   }
   // 如果没有明确指定返回对象,则默认返回res,这个res就是实例对象
   return res;
 }
 // 通过new声明创建实例,这里的p1,实际接收的正是new中返回的res
 const person1 = New(Person, 'tom', 20);//等同于New Person
 console.log(person1.getName());
 // 当然,这里也可以判断出实例的类型了
 console.log(person1 instanceof Person); // true

使用new命令时,它后面的函数依次执行下面的步骤。

  1. 创建一个空对象,作为将要返回的对象实例。
  2. 将这个空对象的原型,指向构造函数的prototype属性。
  3. 将这个空对象赋值给构造函数内部的this关键字。
  4. 开始执行构造函数内部的代码。
__proto__

当一个实例对象被创建时,这个构造函数将会把它的属性prototype赋给实例对象的内部属性__proto__。proto是指向构造函数原型对象的指针。

constructor

prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。

 function P() {}
 P.prototype.constructor === P // true

由于constructor属性定义在prototype对象上面,意味着可以被所有实例对象继承。

 function P() {}
 var p = new P();
 ​
 console.log(p.constructor === P); // true
 console.log(p.constructor === P.prototype.constructor); // true
 console.log(p.hasOwnProperty('constructor')); // false
 console.log(P.prototype.hasOwnProperty('constructor'));

上面代码中,p是构造函数P的实例对象,但是p自身没有constructor属性,该属性其实是读取原型链上面的P.prototype.constructor属性。

instanceof

instanceof 是用来判断 A 是否为 B 的实例(不能判断一个对象实例具体属于哪种类型)

表达式为:A instanceof B。如果 A 是 B 的实例,则返回 true,否则返回 false。

在这里需要特别注意的是:instanceof 检测的是原型,我们用一段伪代码来模拟其内部执行过程:

 instanceof (A,B) = {
     varL = A.__proto__;
     varR = B.prototype;
     if(L === R) {
         // A的内部属性 __proto__ 指向 B 的原型对象
         return true;
     }
     return false;
 }

从上述过程可以看出,当 A 的 proto 指向 B 的 prototype 时,就认为 A 就是 B 的实例,我们再来看几个例子:

 [] instanceof Array; // true
 {} instanceof Object;// true
 newDate() instanceof Date;// true
  
 function Person(){};
 new Person() instanceof Person;
  
 [] instanceof Object; // true
 newDate() instanceof Object;// true
 newPerson instanceof Object;// true

虽然 instanceof 能够判断出 [ ] 是Array的实例,但它认为 [ ] 也是Object的实例

我们来分析一下 [ ]、Array、Object 三者之间的关系:

从 instanceof 能够判断出 [ ].proto 指向 Array.prototype,而 Array.prototype.proto 又指向了Object.prototype,最终 Object.prototype.proto 指向了null,标志着原型链的结束。因此,[]、Array、Object 就在内部形成了一条原型链:

img

从原型链可以看出,[] 的 proto 直接指向Array.prototype,间接指向 Object.prototype,所以按照 instanceof 的判断规则,[] 就是Object的实例。依次类推,类似的 new Date()、new Person() 也会形成一条对应的原型链 。因此,instanceof 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型。

判断是否是数组

 [] instanceof Array; // true

判断某个对象是否是某个构造函数的实例

 function a(){}
 let b = new a()
 //判断实例的构造函数
 console.log(b instanceof a) //true

继承

blog.csdn.net/qq_42926373…

首先创建一个构造函数,并为其设置私有属性和公有属性。

 // 定义一个人类
 function Person(name) {
   // 属性
   this.name = name;
   // 实例方法
   this.sleep = function () {
     console.log(this.name + "正在睡觉!");
   };
 }
 // 原型方法
 Person.prototype.eat = function (food) {
   console.log(this.name + "正在吃:" + food);
 };
 ​
原型链继承

重点圈起来:将父类实例赋值给子类原型对象

 function Super(name, age) {
     this.name = name;
     this.age = age;
 }
 // 原型继承
 Sub.prototype = new Super();

优点

简单易于实现,父类的新增的方法与属性子类都能访问。

缺点

1)可以在子类中增加实例属性,如果要新增加原型属性和方法需要在 new 父类构造函数的后面

2)创建子类实例时,不能向父类构造函数中传参数。

构造继承

重点圈起来:执行父构造,将This指向本身,拉取父私有属性

 function Super(name, age, score) {
   this.name = name;
   this.age = age;
   this.handle1 = () => {
     console.log(this);
   };
 }
 Super.prototype.score = 222;
 Super.prototype.handle2 = () => {
   console.log(this);
 };
 function Sub(name, age, sex, score) {
   Super.call(this, name, age, score);
   this.sex = sex;
 }
 ​
 const obj = new Sub(1, 2, 3);
 console.log(obj.name, obj.score);//1 undefined
 obj.handle1();//Sub { name: 1, age: 2, handle1: [λ], sex: 3 }
 obj.handle2();//obj.handle2 is not a function

优点

只需要继承父类的属性时这种方式很简单。

缺点

只能继承父类自己的属性,父类原型上的属性与方法也不能继承。

组合继承
 function Super(name, age, score) {
   this.name = name;
   this.age = age;
 }
 Super.prototype.score = 222;
 function Sub(name, age, sex) {
   Super.call(this, name, age);
   this.sex = sex;
 }
 // 原型继承
 Sub.prototype = new Super();
 // 构造函数指向
 // Sub.prototype.constructor = Sub;//需要赋值构造函数
 const obj = new Sub(1, 2, 3, 4);
 console.log(obj.name, obj.score);//1 222
 console.log(obj.__proto__);//Super { name: undefined, age: undefined }

优点

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点。而且,使用 instanceof 操作符和isPrototype()方法也能够用于识别基于组合继承创建的对象。

缺点

会调用两次父类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。