JavaScript神奇的面向对象特性(上)

作者: dongwenbo | 来源:发表于2017-01-19 15:04 被阅读168次

    本文写作时长5小时

    OO语言都有一个特点,就是都存在类的概念。通过类自定义类型,创建对象实例。和这些OO语言不同,JavaScript中是没有类的,又或者说原型对象就是JavaScript中的类。但是没有类的JavaScript是如何创建对象呢?退一步讲,在JavaScript中,对象到底是什么?

    理解对象的本质

    ECMA中关于对象的定义

    对象就是无序属性的集合,属性的值可以是基本值,函数或者其他对象

    通过JavaScript提供的Ojbect()函数,可以创建出一个简单的对象实例

    var person = new Object();
    person.name = "fuckJapan";
    person.age = 21;
    person.sayHello = function () {
        alert(this.name)
    }
    

    创建了一个person对象,person有三个属性,其中nameage是基本值,sayHello是一个函数。

    这种写法可以用字面值对象重写为:

    var person = {
        name:"fuckJapan",
        age:21,
        sayHello:function () {
            alert(this.name)
        }
    };
    

    这种语法看起来更加 封装

    上面两种创建对象的方式,有点像手工作坊,每创建一次对象都要重新给对象添加属性,赋值,不胜其烦。怎么解决?进工厂,上生产线。

    工厂模式

    工厂模式抽象出创建对象的工程。既然JavaScript中没有类,所以就用函数代替,类似这样:

    function createPerson(name, age, job) {
        var obj = new Object();
        obj.name = name;
        obj.age = age;
        obj.job = job;
        obj.sayHello = function () {
            alert(this.name);
        }
        return obj;
    }
    var person1 = createPerson("fuckJapan1",21,"student");
    var person2 = createPerson("fuckJapan2",22,"teacher");
    

    有了生产线,工人们再也不用亲自动手了,直接把原料放入函数,duang!一个person,两个person,统统new出来。工人解放了双手,提高了生产力,效率蹭蹭上升。

    我是谁,我来自哪里?

    虽然工厂模式解决了创建多个类似对象的问题,但是却没有解决对象识别的问题。怎么办?

    构造函数模式

    构造函数和普通的函数没有任何区别,只是用new调用时会产生特殊的效果。另外构造函数有一些约定的规定,比如首字母大写。

    function Person(name, age, job) {
        this.name = name;
        this.age = age;
        this.job = job;
        this.sayHello = function () {
            alert(this.name);
        }
    }
    var person1 = new Person("fuckJapan1",21,"student");
    var person2 = new Person("fuckJapan2",22,"teacher");
    

    这种模式中,函数中并没有显示创建对象,而是使用了 this 关键字给对象实例添加属性。在构造函数的结尾也没有return。在调用时,使用了new关键字。在用new调用构造函数时,一共做了这么几件事:

    • 分配对象内存,创建新对象
    • 将这个对象和this绑定,this指向这个对象
    • 执行构造过程并返回对象(在构造函数末尾返回任何东西都不会起作用)

    为什么这种模式解决了对象识别的问题呢?别着急,这是因为在person1person2中还包含一个名为constructor的属性,这个属性指向构造函数Person。从此person1person2不再是三无产品,是可溯源的正品,它们是由一个名为Person的构造工厂生产的。

    alert(person1.constructor == Person)//true
    alert(person2.constructor == Person)//true
    alert(person1.constructor == person2.constructor)//true
    
    alert(person1 instanceof Person)//true
    alert(person2 instanceof Person)//true
    alert(person1 instanceof Object)//true
    alert(person2 instanceof Object)//true
    

    person1person2为什么都是Object类型,这是因为在JavaScript中所有的对象都有一个共同的祖先Object

    不走寻常路,把构造函数当做普通函数调用

    Person("fuckJapan",23,"student");
    alert(window.name);//fuckJapan
    

    由于如果在浏览器中执行,则构造函数中的属性被添加到window对象中

    指定作用域

    var obj = new Object();
    Person.call(obj,"fuckJapanAgain",25,"student");
    alert(obj.name);//fuckJapanAgain
    alert(obj instanceof Person)//false
    

    这种偷鸡摸狗的调用方法,就会导致生产出来的对象没有厂家

    构造函数虽然解决了对象的识别问题,但是却没有解决对象方法的重复创建问题,person1person2中的sayHello方法是两个完全不同的方法,这是完全没有必要的

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

    补救措施,将方法拿到外面,指向全局函数

    function Person(name, age, job) {
        this.name = name;
        this.age = age;
        this.job = job;
        this.sayHello = sayHello;
    }
    
    function sayHello() {
        alert(this.name);
    }
    

    暂时解决了方法重复创建的问题,可是,把自己的方法拿到外面这种方式总是不妥的,全局作用域中的函数却是特定对象的方法,这太诡异了。而且方法少了还好说,多了就太乱了。

    原型模式

    每个函数都有一个名为prototype的属性,指向函数的原型对象,类似OC中的类对象。原型对象中保存着创建对象实例共享的方法和属性。

    function Person() {
    }
    Person.prototype.name = "fuckJapan";
    Person.prototype.age = 29;
    Person.prototype.job = "student";
    Person.prototype.sons = ["son1","son2"];
    Person.prototype.sayName = function () {
        alert(this.name);
    }
    var person1 = new Person();
    var person2 = new Person();
    alert(person1.name);//fuckJapan
    alert(person2.name);//fuckJapan
    alert(person1.sayName == person2.sayName);//true
    

    构造函数变成了空函数,原型对象可以看做是对象的模板,所有对象生成时,默认都有这些属性和对应的值,其中引用类型是所有对象共享,非引用类型都是独立的存储。这样虽然解决了每个对象实例方法重新创建的问题,但又带来了新的问题,数据紊乱,其实不光这样一个问题,还有创建出的对象属性还需要一个个赋值,构造函数根本没什么用

    person1.name = "fuckJapanAgain";
    alert(person1.name);//fuckJapanAgain
    alert(person2.name);//fuckJapan
    

    基本类型的并没有受影响

    person1.sons.push("son3");
    alert(person1.sons);//son1,son2,son3
    alert(person2.sons);//son1,son2,son3
    

    由于引用类型是所有对象共享,所以数据乱了

    说了这么多了,这个也有问题,那个也有问题,那么到底该怎么做呢?

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

    在构造函数中创建每个对象需要单独使用的属性,比如sons。在原型中创建所有对象共享的属性,比如sayHello

    对象实例中在构造函数中创建的属性,每个都保留单独的副本,而在原型中创建的引用类型属性都共享同一份引用。同时这种模式还支持传递参数,可谓一举两得。

    function Person(name,age,job) {
        this.name = name;
        this.age = age;
        this.job = job;
        this.sons = ["son1","son2"];
    }
    
    Person.prototype = {
        constructor:Person,
        sayHello:function () {
            alert(this.name);
        }
    }
    
    var person1 = new Person("fuckJapan1",21,"student");
    var person2 = new Person("fuckJapan2",26,"teacher");
    
    person1.sons.push("son3");
    alert(person1.sons);//son1,son2,son3
    alert(person2.sons);//son1,son2
    
    alert(person1.sayHello == person2.sayHello);//true
    

    所有问题差不多都几乎是完美解决,这就是JavaScript中神奇的『类』。

    除此之外,还有动态原型模式寄生构造函数模式稳妥构造函数模式。读者自己去研究。

    说到面向对象,必须要说的就是继承,JavaScript中的「类」已经很神奇了,作为面向对象特性之一的继承则更加神奇,各种花式继承干到你哭。

    欲知继承如何,且听下回分解...

    相关文章

      网友评论

      • cbd16aeb86b3:赞😀
      • Noah1985:从其他面向对象语言入坑JS。。。。各种不适应。搞个继承都有N种方式,每种都有其优缺点。。。这些都还得记。
        幸好的是。。。ES6后有了class,有了let,有了const。。。
        dongwenbo:小菜还没学会这些,刚看到面向对象
      • 掉了西红柿皮_Kee:沙发沙发!:heart_eyes:
        dongwenbo:@来看评论 :smile:

      本文标题:JavaScript神奇的面向对象特性(上)

      本文链接:https://www.haomeiwen.com/subject/mpfbbttx.html