js面向对象详解

132 阅读10分钟

1.创建对象的方式

1.1 内置构造函数方式

//<!--代码的复用性差, 代码的冗余度高-->
var p1 = new Object();
p1.name = 'sz';
p1.run = function () {
    console.log(this.name + 'pao');
}

var p2 = new Object();
p2.name = 'zs';
p2.run = function () {
    console.log(this.name + 'pao');
}
console.log(p1.name);
p1.run();
console.log(p2.name);
p2.run();

1.2 字面量的方式

  //<!--代码的复用性差, 代码的冗余度高-->
var p1 = {name: 'sz', run: function () {
    console.log(this.name + 'pao');
}}

var p2 = {name: 'zs', run: function () {
    console.log(this.name + 'pao');
}}

console.log(p1.name);
p1.run();
console.log(p2.name);
p2.run();

1.3 简单工厂函数的方式

顾名思义,工厂模式就是像工厂一样来创建对象。但这样的解释似乎有点欠妥,高大上一点,工厂模式其实是软件领域中一种广为人知的一种设计模式,这种模式抽象了创建具体对象的过程。开发人员发明了一种函数,用函数来大量创建对象的方法;

function Person(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 = Person("Nicholas", 29, "Software Engineer");
var person2 = Person("Greg", 27, "Doctor");

缺点:1.工厂模式虽然解决了创建多个相似对象造成的代码重复问题,但仍未解决对象类型的区分问题(怎样知道一个对象的具体类型);通过工厂模式创建出的对象,其类型都是Object,如果能把下例中创建出的对象标记为Person类型就好了,2.通过工厂模式创建出的对象还存在着一个很严重的问题,那就是内存浪费。每当调用一次Person()函数创建一个对象,就会在其内部创建一个函数实例。在前面的例子中,person1和person2都有一个名为sayName()的方法,但person1.sayName()和person2.sayName()并不是引用的同一个函数实例,而是不同的实例,因为其中的

  o.sayName = function() {
   alert(this.name);
};

o.sayName = new Function("alert(this.name)");

在逻辑上是完全等价的。为了证明person1.sayName()和person2.sayName()引用的是不同的函数实例,有:

  alert(person1.sayName == person2.sayName)      //false

因此,若一个自定义对象类型中要是有多个方法,那么通过工厂模式定义出多个对象造成的内存浪费就可想而知了。

1.4 自定义构造函数的方式

为了解决对象的类型问题,可以使用构造函数模式,JavaScript中的构造函数可以用来创建特定类型的对象:

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

首先需要明确什么样的函数称为构造函数,即通过new操作符+函数名的方式来创建对象的函数,就叫做构造函数。

//构造函数内部的执行步骤
function Person(name, age, runFunc) {
     // 1. 自动的创建一个空的对象, 并且, 把这个对象的地址, 给this -> 新对象(不需要自己
     写,自带的)
    var this = new Object();
    // 2. 可以通过this, 给新的空对象, 进性添加属性, 或者方法(我们来做的事情)
    this.name = name;
    this.age = age;
    this.run = runFunc;
     // 3. 返回这个最新创建的对象(不需要自己写,自带的)
    return this;
}

1.4.1 关于构造函数的返回值

  1. 如果不写, -> 直接返回默认创建的新对象
  2. 如果返回this -> 直接返回默认创建的新对象
  3. 如果返回的是基本数据类型: 直接返回默认创建的新对象
  4. 如果返回到是对象 -> 直接把这个对象, 返回给外界, 外界接受到的就是这个函数内部返回的对象
 function Person(name, age) {
    this.name = name;
    this.age = age;
//        return this;
    return [1,2,3];
}
// 类型匹配才接受-> 对象
var p = new Person('sz', 18);
console.log(p);  //[1,2,3]

1.4.2 ECMAScript-异常处理

ES3开始引入了 try-catch 语句,是 JavaScript 中处理异常的标准方式。 语法:

    try{ 
    //可能发生异常的代码 
    }catch(error){ 
    //发生错误执行的代码 
    }
 function devide(a, b) {
   /* if(0 === b){ // 分子为零
       // 抛出异常
       throw '分子不能为零!'
    }*/
    return a /b;
}

try {
//可能会出错的代码放到try里边
   console.log(devide(10, 0)); //Infinity
}catch (e) {
//出错时throw抛出的异常提示
   console.log(e); //分子不能为零!
}finally {
//不管有错没错, 都会进来执行
   console.log('不管有错没错, 都会进来执行, 一般在这里释放资源');
}

console.log(1111);
console.log(devide(10, 0)); //Infinity

1.4.3 构造函数面向对象写法

1.4.3.1 构造函数面向对象-默认写法
function Dog(name, age, dogFriends) {
    // 属性
    this.name = name;
    this.age = age;
    this.dogFriends = dogFriends;

    // 行为
    this.eat = function (someThing) {
        console.log(this.name + '吃' + someThing);
    };

    this.run = function (someWhere) {
        console.log(this.name + '跑' + someWhere);
    }
}

// 产生对象
var smallDog = new Dog('小花', 1);
console.log(smallDog.name, smallDog.age);
1.4.3.2 构造函数面向对象-传参优化

把形参放进一个对象中,调用时传入一个option的对象即可,(这样就没必要传入很多个形参,只需要写一个形参option)

function Dog(option) {
    // 属性
    this.name = option.name;
    this.age = option.age;
    this.dogFriends = option.dogFriends;

    // 行为
    this.eat = function (someThing) {
        console.log(this.name + '吃' + someThing);
    };

    this.run = function (someWhere) {
        console.log(this.name + '跑' + someWhere);
    }
}

// 产生对象
var smallDog = new Dog({name: '小花', age: 1, dogFriends: ['球球', '嘎嘎嘎']});
console.log(smallDog.name, smallDog.age); //小花 1
1.4.3.3 构造函数面向对象-改进写法(把构造函数方法写在原型中)

把方法放入原型prototype中,是因为通过new生成的实例,相当于是重新开辟了一个堆区,虽然是同类型,拥有类似的属性和方法,但是这些属性和方法,并不是相同的

 function Person(name,age){
    this.name = name;
    this.age = age;
    this.run = function(){
        console.log('跑步')
    };
 };

let p1 = new Person("张三",18);
let p2 = new Person("李四",17);

console.log(p1.run === p2.run) // fasle

上面这种写法创建出来的实例对象每一个都重新复制了构造函数中属性和方法,无疑是非常耗费性能的,所以我们把方法放入构造函数的原型prototype中,让所有的实例去继承这些方法

function Dog(option) {
    // 属性
    this.name = option.name;
    this.age = option.age;
    this.dogFriends = option.dogFriends;

    // 行为
  /*  this.eat = function (someThing) {
        console.log(this.name + '吃' + someThing);
    };

    this.run = function (someWhere) {
        console.log(this.name + '跑' + someWhere);
    }*/
}

Dog.prototype.eat = function (someThing) {
    console.log(this.name + '吃' + someThing);
};

Dog.prototype.run = function (someWhere) {
    console.log(this.name + '跑' + someWhere);
};

// 产生对象
var smallDog = new Dog({name: '小花', age: 1, dogFriends: ['球球', '嘎嘎嘎']});
var bigDog = new Dog({name: '大花', age: 11, dogFriends: ['球球', '嘎嘎嘎', 'hh']});

console.log(smallDog.eat === bigDog.eat); //true

既然我们可以把方法写在原型中让实例去继承,当然属性也可以,另外我们一个一个写太过于凌乱,可以把所有方法和属性放到统一的原型对象中去

 function Dog(option) {
    this._init(option)
 }
Dog.prototype = {
    _init: function(option){
        // 属性
        this.name = option.name;
        this.age = option.age;
        this.dogFriends = option.dogFriends;
    },

    eat: function (someThing) {
        console.log(this.name + '吃' + someThing);
    },

    run: function (someWhere) {
        console.log(this.name + '跑' + someWhere);
    }
 };
 // 产生对象
var smallDog = new Dog({name: '小花', age: 1, dogFriends: ['球球', '嘎嘎嘎']});
var bigDog = new Dog({name: '大花', age: 11, dogFriends: ['球球', '嘎嘎嘎', 'hh']});


console.log(Dog.prototype);
console.log(smallDog.eat === bigDog.eat); //true

1.4.4 构造函数创建出来的实例对象类型验证

instanceof的用法,a instanceof b?alert("true"):alert("false"); //a是b的实例?真:假

 function Person(name, age) {
    this.name = name;
    this.age = age;
}
function Dog(name, age) {
    this.name = name;
    this.age = age;
}
function Cat(name, age) {
    this.name = name;
    this.age = age;
}

var p = new Person('sz', 18);
var d = new Dog('小花', 19);
var c = new Cat('小猫', 16);

console.log(p instanceof Person);//true
console.log(d instanceof Dog);//true
console.log(c instanceof Cat);//true

console.log(p instanceof Object);//true
console.log(d instanceof Object);//true
console.log(c instanceof Object);//true

console.log(p instanceof Dog);//false
console.log(d instanceof Cat);//false
console.log(c instanceof Person);//false

1.4.5 访问构造函数原型对象的方式

  1. prototype的方式 ,2. 实例对象的__proto__的方式
function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.run = function () {
    console.log('跑');
};

// 方式一
console.log(Person.prototype);

// 方式二
var p = new Person();
console.log(p.__proto__);

1.4.6 hasOwnProperty和in属性操作

in 判断一个对象, 是否拥有某个属性(如果对象身上没有, 会到原型对象里面查找)

function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.address = '上海';

var p = new Person('张三', 18);
// 一定要注意, 不能直接写名称, 不然会被当做变量名处理, 要以字符串的形式来写
// console.log(name in p);  // 为啥是false?
console.log('name' in p);  //true
console.log('address' in p); //true

hasOwnProperty与in作用相似: 不同点是只到对象自身查找

 function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.address = '上海';

var p = new Person('sz', 18);
console.log(p.hasOwnProperty('name'));//true
console.log(p.hasOwnProperty('address')); //false

1.4.7 isPrototypeOf用法

isPrototypeOf函数方法是返回一个布尔值,指出对象是否存在于另一个对象的原型链

//例如:判断Person.prototype这个对象是否存在于p的原型链中
function Person() {}
var p = new Person();
console.log(Person.prototype.isPrototypeOf(p)); // true

1.4.8 construnctor: 构造器

作用:找到产生这个实例对象的构造函数

function Person() {}
var p = new Person();
console.log(p);//Person对象

// 可以沿着这条线找到类型名称
console.log(p.constructor.name);//Person

Person.prototype = {
    constructor: Person,
    name: 'sz',
    age: 18
};

var p2 = new Person();
console.log(p2.constructor.name);//Person

1.4.9 原型链继承

1.4.9.1 原型链继承概念

继承概念解析:继承就是让子类拥有父类的资源

举例:

类: 动物, 狗类, 哈士奇

对于狗类来说:动物是它的父类型,而哈士奇类是它的子类型

对于哈士奇类来说:狗类是它的父类型,动物类是它的超类型.

继承的意义:

1.减少代码冗余

2.方便统一操作

弊端:耦合性比较强(原型链中其中一环出问题,整个链条就会出问题)

1.4.9.2 原型链图解

原型链:

每个函数都能构建出一个对象,这个对象内部有个属性__proto__指向这个函数的原型对象

原型对象本质也是一个对象,也是由另外一个构造函数构造出来,也指向那个构造函数的原型对象

以上,形成一个链式的结构,就称为是原型链

原型链检索规则:

1.对象 . 属性的方法去访问属性的时候,先查找有没有对应的实例属性,如果有那么就直接使用

2.如果没有,那么就去该对象的原型对象上面去找,如果有那么就直接使用

3.如果没有,那么就接着查找原型对象的原型对象,如果有,那么就直接使用,

4.如果没有,那么就继续上面的搜索过程

5.直到搜索到Object.prototype为止,如果还是没有找到就返回undefined或者是报错

6.注意:原型链搜索的路径越长,查询属性所花费的时间就越多,就近原则

var arr = [1, 2, 3];
console.log(arr.constructor.name); // Array

console.log(arr.__proto__.constructor.name); // Array

图解 arr对象与Array的继承

深层次继承图解(arr-Array-function)

console.log(Array.__proto__.constructor.name);//Function

console.log(Function.__proto__.constructor.name);//Function


深层次继承图解(arr-Array-function-object)

//Function.prototype是Object构造函数的实例
console.log(Function.prototype.__proto__.constructor.name);//Object
//Object.prototype原型为null
console.log(Object.prototype.__proto__);//null
//Object构造函数是function构造函数的实例
console.log(Object.__proto__.constructor.name);//Function

深层次继承图解(arr-Array-function-object) 添加 Array.prototype链条

   //Array.prototype是Object构造函数的实例
   console.log(Array.prototype.__proto__.constructor.name);//Object

继承实例

有两个函数person(父构造函数)与student(子构造函数)(如图)

image.png

问题:目前来看,两个构造函数没有任何关系,子对象无法访问到父类的任何东西

解决方案:修改原型指向(如下图)

image.png

问题:子对象只能访问到父类原型上的属性和方法,不能访问到到父类的实例属性和方法。

解决方案:构造父类的实例,并设置为子类的原型对象(如下图)

image.png

问题:类型问题

解决方案:修复constructor指针即可(如下图)

image.png

到此为止,原型链继承已经结束,

问题:继承过来的实例属性, 如果是引用类型, 一旦发生更改,会被多个子类的实例共享。

解决方案:利用原型链 + 借助构造函数(称作组合式继承),在子构造函数内部, 调用父构造函数,但是需要修改this指向

    /**
     * 构造函数Person
     * @constructor
     */
    function Person() {
       this.name = '张三';
       this.pets = ['java''Python'];
    }
    Person.prototype.run = function () {
        console.log('跑');
    };
    /**
     * 构造函数Student
     * @constructor
     */
    function Student() {
        Person.call(this);
        this.num = 'itlike.com';
        /*this.name = '张三';
        this.pets = ['java', 'Python'];*/
       //  Person.call(this);
    }
    // 1. 构造父类的实例
    var p = new Person();
    // 2. 并设置为子类的原型对象
    Student.prototype = p;
    // 3.修复constructor指针即可
    Student.prototype.constructor = Student;
    // 实例对象
    var stu = new Student();
    console.log(stu.name);
    console.log(stu.pets);

问题:父类构造函数的参数无法修改

解决方案:父类构造函数, 需要设置接收可变参数,子类构造函数在调用父类构造函数的时候, 传递参数即可(如下:)

    /**
     * 构造函数Person
     * @constructor
     */
    function Person(name, pets) {
       this.name = name;
       this.pets = pets;
    }
    Person.prototype.run = function () {
        console.log('跑');
    };
    /**
     * 构造函数Student
     * @constructor
     */
    function Student(num, name, pets) {
        Person.call(this, name, pets);
        this.num = num;
    }
    // 1. 构造父类的实例
    var p = new Person();
    // 2. 并设置为子类的原型对象
    Student.prototype = p;
    // 3.修复constructor指针即可
    Student.prototype.constructor = Student;
    // 实例对象    
    var stu = new Student();     
    console.log(stu.name);     
    console.log(stu.pets);

问题:父类属性重复(实例上有一份,原型对象上有一份)

解决方案:1. 创建出来一个空对象实例(T),指向父类构造函数的原型对象. 2.修改子构造函数原型为空对象实例(T)(称作寄生式组合继承)

    /**
     * 构造函数Person
     * @constructor
     */
    function Person(name, pets) {
       this.name = name;
       this.pets = pets;
    }
    Person.prototype.run = function () {
        console.log('跑');
    };
    /**
     * 构造函数Student
     * @constructor
     */
    function Student(num, name, pets) {
        // 1. 借调
        Person.call(this, name, pets);
        this.num = num;
    }
    // 1. 构造函数
    function Temp() {}
    // 2. 指向
    Temp.prototype = Person.prototype;
    // 3. 产生对象
    var stuPro = new Temp();
    // 4. 修改Student的指向
    Student.prototype = stuPro;
    // 5. 增强动作
    stuPro.constructor = Student;
    Student.prototype.run=function () {
        console.log('跑');
    };

image.png