感觉最难的部分就是面向对象了,大学期间学习的是面向过程的c/c++,工作之后也没有深入了解过面向对象,通过这次的学习和回顾,也算是对面向对象有了新的认识。不过,就我在书上学到了结合个人理解随便说说,很可能有理解错误或者不准确的地方。js的对象和其他语言的对象并不完全一样,可以理解为散列表。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。

    首先需要知道一个定义,特性。我觉得可以理解为对象的属性的属性,用于描述属性的各种特征。可以用来对对象的属性进行管理,比如属性数据的读写和访问权限。但是我看完了之后,觉得这个功能虽然强大,不过目前用不到,以后碰到的时候再补充吧。下面好好讲讲创建对象和继承。

一,创建对象

    前面一篇文章提到了创建对象的一些方法,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");
        var person2 = createPerson("Greg", 27, "Doctor");
        
        person1.sayName();   //"Nicholas"
        person2.sayName();   //"Greg"

    但是这样创建的话,每个对象的类型都是object,无法识别区别。

2,构造函数模式

    像Object和Array这样的原生构造函数,可以通过new调用来直接创建对象。而我们也可以自定义构造函数,从而自定义对象类型的属性和方法。

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");
        
        person1.sayName();   //"Nicholas"
        person2.sayName();   //"Greg"
        
        alert(person1 instanceof Object);  //true
        alert(person1 instanceof Person);  //true
        alert(person2 instanceof Object);  //true
        alert(person2 instanceof Person);  //true
        
        alert(person1.constructor == Person);  //true
        alert(person2.constructor == Person);  //true
        
        alert(person1.sayName == person2.sayName);  //false

用new操作符调用构造函数实际上经过了四步:(1),创建一个新对象。(2),将构造函数的作用域赋给新对象。(3),执行构造函数的代码,给新对象增加属性。(4),返回新对象。这两个生成的对象都有一个构造函数属性constructor,指向构造函数Person。对于构造函数这一类特殊函数,还有值得说明的地方。

首先,构造函数是可以直接调用的,直接把属性赋给window对象。

function Person(name, age, job){
            this.name = name;
            this.age = age;
            this.job = job;
            this.sayName = function(){
                alert(this.name);
            };
        }
        
        var person = new Person("Nicholas", 29, "Software Engineer");
        person.sayName();   //"Nicholas"
        
        Person("Greg", 27, "Doctor");  //adds to window
        window.sayName();   //"Greg"
        
        var o = new Object();
        Person.call(o, "Kristen", 25, "Nurse");
        o.sayName();    //"Kristen"

其次,构造函数有一个问题就是,如果像上面那么定义的话,会定义很多sayName函数,但是都是做同一件事情(每次实例构造函数都会执行内部代码)。明显不太好,把方法定义放在函数外部可以解决这个问题。

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

    不过这样也有问题,直接打字书上的原话:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是,如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个定义的引用类型就没有丝毫的封装性可言了。好在这些问题都可以通过原型模式解决。

3,原型模式
    我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。实际上,prototype就是原型对象,不必在构造函数中定义对象实例的信息,而是可以直接把这些信息添加到原型对象中。

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
        
        alert(Person.prototype.isPrototypeOf(person1));  //true
        alert(Person.prototype.isPrototypeOf(person2));  //true
        
        //only works if Object.getPrototypeOf() is available
        if (Object.getPrototypeOf){
            alert(Object.getPrototypeOf(person1) == Person.prototype);  //true
            alert(Object.getPrototypeOf(person1).name);  //"Nicholas"
        }

    个人理解,这种方法就是改良工厂方法,相当于把工厂方法中的新建对象和赋值的过程,变成了把原型对象作为原型对象属性赋给新对象的过程。同时我们这个时候修改实例的属性时,会在实例下添加属性。而在我们调用实例的属性时,会先查找新对象新增加的属性,然后找找原型对象的属性。可以通过in,判断属性是否存在。可以通过hasOwnProperty(),判断属性是在实例还是原型。(就不举例了)

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();
        var person2 = new Person();
        
        person1.name = "Greg";
        alert(person1.name);   //"Greg" ?from instance
        alert(person2.name);   //"Nicholas" ?from prototype

可以直接重写原型对象,这样更方便,不过这样一来,本来从构造函数获取的原型对象的构造函数属性就变成Object了,需要重定义一下。

function Person(){
        }
        
        Person.prototype = {
            constructor : Person,
            name : "Nicholas",
            age : 29,
            job: "Software Engineer",
            sayName : function () {
                alert(this.name);
            }
        };

        var friend = new Person();
        
        alert(friend instanceof Object);  //true
        alert(friend instanceof Person);  //true
        alert(friend.constructor == Person);  //true
        alert(friend.constructor == Object);  //false

与此同时,重写原型对象会切断现有原型和任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型。

然而,原型对象方法有一个致命缺点,对于包含引用类型的属性,修改实例对象会影响原型对象从而影响所有对象。

function Person(){
        }
        
        Person.prototype = {
            constructor: Person,
            name : "Nicholas",
            age : 29,
            job : "Software Engineer",
            friends : ["Shelby", "Court"],
            sayName : function () {
                alert(this.name);
            }
        };
        
        var person1 = new Person();
        var person2 = new Person();
        
        person1.friends.push("Van");
        
        alert(person1.friends);    //"Shelby,Court,Van"
        alert(person2.friends);    //"Shelby,Court,Van"
        alert(person1.friends === person2.friends);  //true

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

    讲了这么多有问题的方法,讲一个能用而且最常用的方法吧。组合了构造函数模式和原型模式,构造函数模式用于定义实例属性,原型模式用于定义方法和共享属性。

function Person(name, age, job){
            this.name = name;
            this.age = age;
            this.job = job;
            this.friends = ["Shelby", "Court"];
        }
        
        Person.prototype = {
            constructor: Person,
            sayName : function () {
                alert(this.name);
            }
        };
        
        var person1 = new Person("Nicholas", 29, "Software Engineer");
        var person2 = new Person("Greg", 27, "Doctor");
        
        person1.friends.push("Van");
        
        alert(person1.friends);    //"Shelby,Court,Van"
        alert(person2.friends);    //"Shelby,Court"
        alert(person1.friends === person2.friends);  //false
        alert(person1.sayName === person2.sayName);  //true

5.动态原型模式

    这个我看了几遍都没看懂,看了知乎上面的一个解释瞬间明白了。可以注意到第四种组合方法里面,原型对象是采用重写方法的,当必须采用字面量方法时,那就会碰到构造函数模式的问题,多次定义相同函数,这个时候加个判断问题就迎刃而解了。

function Person(name, age, job){
        
            //properties
            this.name = name;
            this.age = age;
            this.job = job;
            
            //methods
            if (typeof this.sayName != "function"){
            
                Person.prototype.sayName = function(){
                    alert(this.name);
                };
                
            }
        }

        var friend = new Person("Nicholas", 29, "Software Engineer");
        friend.sayName();

此外还有寄生构造函数模式和稳妥构造函数模式就不说了。

二,继承

    继承和创建有很多类似的地方,但是现在用的少,以后想起来再写吧。