JS之面向对象和原型

5401241-aff38458e9f5cd50.png

一、创建对象的方式

0、Object构造函数和字面量方式

var obj1={name:'obj1'}//字面量
var obj2=new Object({name:'obj2'})
var obj2=new Object//不推荐

缺点:使用同一个接口创建很多对象,会产生大量的重复代码

1、 工厂模式

若使不同的对象拥有相同的属性和方法,如果每个对象都单独定义一遍,会产生大量代码。工厂模式,就是解决这个问题,而且,每次调用返回的对象相互独立。工厂模式把创建对象封装在一个函数里来实现。

function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = createPerson("Nicholas", 29, "Software Engineer");
person1.sayName()//"Nicholas"
var person2 = createPerson("Greg", 27, "Doctor");
person2.sayName()//"Greg"

千万不要对对象直接复制,下面这样做,box1和box指向同一个对象,其一变,则都变

var box = new Object();
box.age = 100; //创建一个age 属性并赋值
box.run = function () { //创建一个run()方法并返回值
return this.name + this.age + '运行中...';
};
var box1=box;
box1.age=1;
console.log(box.age)//1

工厂模式
优点:解决代码重复(重复化实例)
缺点:无法区分是哪个对象的实例

2、构造函数

与工厂模式的不同:
1.构造函数方法没有显示的创建对象(new Object());
2.直接将属性和方法赋值给this 对象;
3.没有renturn 语句。

function Person(name){
    this.name=name;
    this.sayName=function(){
        console.log(this.name);
    }
}
var p1=new Person('A');
p1.sayName();//A

p1有一个constructor属性,指向Person

console.log(p1.constructor==Person);//true

对象的constructor属性最初是用来标识对象类型的,但是检测对象类型建议使用instanceof。

console.log(p1 instanceof Person);//true
console.log(p1 instanceof Object);//true

构造函数的规范:1、函数名和实例化构造名相同且大写(非强制)2、通过构造函数创建对象,必须用new运算符
构造函数执行过程:
1、当使用了构造函数,并且new构造函数(),那么后台就执行了new Object();
2、将构造函数的作用域给新对象,而函数体内的this就代表new Object()出来的对象。
3、执行构造函数内的代码
4、返回新对象

构造函数优点;解决代码重复,解决对象类别识别问题
缺点:每个方法都要在实例上创造一遍,因为函数也是对象,每定义一个函数,就实例化了一个对象new Function().因此
console.log(p1.sayName==p2.sayName);//false
解决:把函数定义转移到外部,p1,p2共享全局下的同一个sayName()。但是,这样做毫无封装可言

function Person(name){
    this.name=name;
    this.sayName=sayName;
}
function sayName(){
        console.log(this.name);
    }
var p1=new Person('A');
var p2=new Person('b');
console.log(p1.sayName==p2.sayName);//true

3、原型(函数中的一个对象属性)

每个函数有一个prototype属性,这个属性指向函数的原型对象,该属性包含由特定类型的所有实例共享的属性和方法。不妨理解为,prototype是通过调用构造函数而创建的那个实例对象的原型对象。实例通过内部的proto来指向原型不必在构造函数中定义对象信息,而是可以直接将这些信息添加到原型中。

function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true

5401241-aba6a65acdaa2556.png

[[Prototype]]在创建实例时,自动生成,他是一个指针,指向构造函数的原型对象, 实例与构造函数没有直接关系。通过 isPrototypeOf()方法来确定对象之间是否存在这种关系。 实例person1/2和原型的constructor均指向构造函数

console.log(Person.prototype.isPrototypeOf(person1))//true

另一个是Object.getPrototypeOf()获取对象的原型

console.log(Object.getPrototypeOf(person1)==Person.prototype)//true

原型模式的执行流程:
1.先查找构造函数实例里的属性或方法,如果有,立刻返回;
2.如果构造函数实例里没有,则去它的原型对象里找,如果有,就返回;
使用hasOwnProperty()方法来检测一个属性是存在于实例中还是原型中,

console.log(person1.hasOwnProperty('name'))//false
person1.name='w';
console.log(person1.hasOwnProperty('name'))//true
console.log(person1.name)//w

in操作符会在通过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中.

console.log(person1.hasOwnProperty('name'))//false
console.log('name' in person1);//true
person1.name='w';
console.log(person1.hasOwnProperty('name'))//true
console.log('name' in person1);//true

为了让属性和方法更好的封装,可用字面量方式,但是要注意,字面量创建的方式使用constructor属性不会指向构造函数,而是指向Object

function Person(){
}
Person.prototype={
    name:'s',
    sayName:function(){
        console.log(this.name);

    }
}
var p1=new Person();
console.log(p1.constructor==Person);//false
console.log(p1.constructor==Object);//true

字面量方式,需要强制constructor指向构造函数

function Person(){
}
Person.prototype={
   constructor:Person,
    name:'s',
    sayName:function(){
        console.log(this.name);

    }
}
var p1=new Person();
console.log(p1.constructor==Person);//true
console.log(p1.constructor==Object);//false

字面量方式为什么constructor 会指向Object?因为,字面量其实就是新创建了一个对象,执行new Object(),因此该字面量对象的构造函数是Object,原型是Object.Prototype。所以,新对象的constructor 重写了原来的constructor,因此会指向新对象,那个新对象没有指定构造函数,那么就默认为Object。

原型的声明是有先后顺序的,所以,重写的原型会切断之前的原型。

function Person(){
}
Person.prototype={
    constructor:Person,
    name:'s',
    sayName:function(){
        console.log(this.name);

    }
}
var p1=new Person();
Person.prototype={
    constructor:Person,
    name:'h',
    sayName:function(){
        console.log(this.name);

    }
}
var p2=new Person();
p2.sayName()//h
p1.sayName()//s

原型优缺点:
它省略了构造函数传参初始化这一过程,带来的缺点就是初始化的值都是一致的。而原型最大的缺点就是它最大的优点,那就是共享。

原型中所有属性是被很多实例共享的,共享对于函数非常合适,对于包含基本值的属性也还可以。但如果属性包含引用类型,就存在一定的问题,一个实例改动,其他实例的该属性也跟着变了
解决:组合构造函数+原型模式

function Person(){
 this.name=['s','a'];
}
Person.prototype={
    constructor:Person,
    sayName:function(){
        console.log(this.name);

    }
}
var p1=new Person();
p1.name.push('d')
var p2=new Person();
p2.sayName()//['s','a'];p2的name没有随着改变
p1.sayName()// ["s", "a", "d"]

寄生构造函数,其实就是工厂模式+构造函数模式。这种模式比较通用,但不能确定对象关系,所以,在可以使用之前所说的模式时,不建议使用此模式。
在什么情况下使用寄生构造函数比较合适呢?假设要创建一个具有额外方法的引用类型。由于之前说明不建议直接String.prototype.addstring,可以通过寄生构造的方式添加。给js自带对象添加属性和方法

function myString(string) {
var str = new String(string);
str.addstring = function () {
return this + ',被添加了!';
};
return str;
}
var box = new myString('Lee'); //比直接在引用原型添加要繁琐好多
alert(box.addstring());

在一些安全的环境中,比如禁止使用this 和new,这里的this 是构造函数里不使用this,这里的new 是在外部实例化构造函数时不使用new。这种创建方式叫做稳妥构造函数。所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this 的对象。

function Box(name , age) {
var obj = new Object();
obj.run = function () {
return name + age + '运行中...'; //直接打印参数即可
};
return obj;
}
var box = Box('Lee', 100); //直接调用函数
alert(box.run());
//稳妥构造函数和寄生类似。

4、Object.create(proto, [propertiesObject])

proto 新创建对象的原型对象。
propertiesObject可选。如果没有指定为 undefined,则是要添加到新创建对象的可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)对象的属性描述符以及相应的属性名称。这些属性对应Object.defineProperties()的第二个参数。

var p={name:'p'}
var obj=Object.create(p)//p是obj的原型,var x=new Object({a:'d'}),x就是{a:'d'}本身

二、继承

原型链的顶端是Object


5401241-c40763f41e85a0ac.png

1、利用原型链继承

利用原型链实现继承,所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的。大家要记住,所有函数的默认原型都是Object 的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。

function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subproperty = false;
}
//继承了SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true

5401241-c1704d732a626c5b.png

构造函数A,在构造函数A的原型中定义一些属性和方法。在定义一个构造函数B,让B的原型称为A的一个实例,那么B原型会指向A原型,B的实例会继承A原型中的属性和方法,。且指向B原型

构造函数、原型和实例的关系:每个构造函数(prototype属性指向原型函数)都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

记住一点,实例指向构造函数的原型,实例和构造函数之间没有直接关系。

原型链继承的缺点是,父亲中含有引用类型的属性时,由于孩子共享父亲,当其中一个孩子改变引用类型的值时,也会反应到其余的孩子身上。

 function Parent2 () {
          this.name = 'parent2';
          this.play = [1, 2, 3];
      }
      function Child2 () {
          this.type = 'child2';
      }
      Child2.prototype = new Parent2();

      var s1 = new Child2();
      var s2 = new Child2();
      s1.play.push(4);
      console.log(s1.play, s2.play);//(4) [1, 2, 3, 4] (4) [1, 2, 3, 4]

2、利用构造函数的call实现继承

/**
       * 借助构造函数实现继承
       */
      function Parent1 () {
          this.name = 'parent1';
      }
      Parent1.prototype.say = function () {

      };
      function Child1 () {
          Parent1.call(this);//继承parent1
          this.type = 'child1';
      }
 console.log(new Child1(), new Child1().say());//出错

缺点:孩子获取不到父级的原型上的属性和方法

3、组合方式

 function Parent3 () {
          this.name = 'parent3';
          this.play = [1, 2, 3];
      }
      function Child3 () {
          Parent3.call(this);
          this.type = 'child3';
      }
      Child3.prototype = new Parent3();
      var s3 = new Child3();
      var s4 = new Child3();
      s3.play.push(4);
      console.log(s3.play, s4.play);

缺点:实例化孩子,父类执行2次

4、组合继承优化1

function Parent4 () {
          this.name = 'parent4';
          this.play = [1, 2, 3];
      }
      function Child4 () {
          Parent4.call(this);
          this.type = 'child4';
      }
      Child4.prototype = Parent4.prototype;//继承构造函数
      var s5 = new Child4();
      var s6 = new Child4();
      console.log(s5, s6);

      console.log(s5 instanceof Child4, s5 instanceof Parent4);//true,true
      console.log(s5.constructor);//Parent4

缺点:无法区分实例是通过父类还是子类创建的

5、组合继承的优化2

    function Parent5() {
        this.name = 'parent5';
        this.play = [1, 2, 3];
    }
    function Child5() {
        Parent5.call(this);
        this.type = 'child5';
    }
    Child5.prototype = Object.create(Parent5.prototype);//借助Object.create的中间对象,同样原型也是Parent5.prototype
    Child5.prototype.constructor = Child5
    var s5 = new Child5();
    console.log(s5 instanceof Child5, s5 instanceof Parent5);//true,true
    console.log(s5.constructor);//Child5

三、new运算符原理

new运算符的原理其实就是构造函数创建的背后流程。


5401241-2f8b22d26475dabf.png

参考资料:JavaScript高级程序设计(第3版)

猜你喜欢

转载自blog.csdn.net/weixin_34248487/article/details/87232687