• 前端基础进阶(九):详解面向对象、构造函数、原型与原型链


    https://segmentfault.com/a/1190000012646488  https://yangbo5207.github.io/wutongluo/

    说明:此处只是记录阅读前端基础进阶的理解和总结,如有需要请阅读上面的链接

    一、对象的定义

    在JavaScript中对象被定义为无序属性的集合,其属性可以是基本类型,也可以是对象,函数


    1.创建对象的两种方式

    1) 使用new关键字

    var o =new Object()

    2)使用字面量形式

      var person = {
            name: "xiaoming",
            age: 13,
            getName: function () { return this .name}
        }

    2.给对象添加属性和方法的两种方法

        //方法1
        var o = new Object();
        o.name = "lilei"; //添加属性name并赋值
        o.getName = function () { return this .name}//添加方法
    
        //方法2
        var person = {
            name: "xiaoming",
            age: 13,
            getName: function () { return this .name}
        }

    3.访问对象属性的两种方式

     //访问name属性的两种方式
        person.name
    
        person["name"]

    如果要同时访问一个对象里面的多个属性可以这样。注:forEach不能写成foreach,否则会报错,并且上一个语句必须有分号结束

        //方法2
        var person = {
            name: "xiaoming",
            age: 13,
            getName: function () { return this.name }
        };//必须写分号,否则下面的forEach会报错
    
         ['name', 'age'].forEach(function (item) {//是forEach,不是foreach
            console.log(person[item]);
        });

    二、工厂模式

    使用上面的方法创建对象很简单,但是如果需要用到两个对象,比如Tom和Jack就不得不把代码类似的代码写两遍,浪费时间,这样工厂模式就出现了。

    工厂模式就是按照给定的模式创建出我们想要的对象,想创建多少就创建多少

     //工厂模式
        function createPerson(name, age) {
            var o = new Object();
            o.name = name;
            o.age = age;
            o.getName = function () { return this.name; };
            return o;
        }
    
        var perTom = createPerson("Tom", 12);
        var perJack= createPerson("Jack",14);

    使用工厂模式虽然简单,但是有两个缺陷:

    1)不能使用instanceof判断对象的实例类型

    var person = {};
        var foo = function () { }
    
        console.log(obj instanceof Object);  // true
        console.log(foo instanceof Function); // true

    2)当有不同的实例对象时,每个对象都要生成一个getName方法,每次都要为getName分配一个单独的内存空间

     三、构造函数

    为了解决第一个问题就需要用到构造函数了。

     //构造函数
        function Person(name, age) {
            this.name = name;
            this.age = age;
            this.getName = function () { return this.name; };
        }
    
        var perNike = new Person("Nike", 13);
        console.log(perNike instanceof Person);//true
        console.log(perNike.getName());//Nike

    构造函数与一般函数并没有本质区别,首字母大写只是约定用来区别构造函数和普通函数的。任何函数都可以当做是构造函数用来产生对象

    new关键字可以产生一个对象,调用new时函数内部执行了如下过程:

    1)产生一个新对象,把函数的this指向这个对象

    2)新对象的原型指向构造函数的原型

    3)给新对象添加属性和方法

    4)返回新对象

    四、原型

    为了解决工厂模式的第二个问题需要用到另一个东西——原型

    每一个函数都有一个prototype属性,该属性指定一个对象,这个对象就是原型。

    从构造函数部分我们指定每一个new出来的示例对象都有一个__proto__属性,这个属性指向构造函数的原型对象,如果我们创建对象时通过prototype属性挂载一些方法和属性,那么实例对象就可以通过__proto__属性访问之前挂载的属性和方法,而每个实例对象都是访问同一个原型对象这样就解决了工厂模式的第二个问题。

    我们把挂载在原型对象上的属性和方法叫做公有属性和公有方法(类似C#的static属性和方法),因为可以被所有实例对象访问;把在构造函数中通过this声明的属性和方法叫做私有属性和方法,因为它只属于一个实例对象。

    可以通过一个例子看构造函数,实例对象和原型三者的关系

    // 声明构造函数
    function Person(name, age) {
        this.name = name;
        this.age = age;
    }
    
    // 通过prototye属性,将方法挂载到原型对象上
    Person.prototype.getName = function() {
        return this.name;
    }
    
    var p1 = new Person('tim', 10);
    var p2 = new Person('jak', 22);
    console.log(p1.getName === p2.getName); // true

     通过图片可以看出构造函数和实例对象都指向原型对象,而原型constructor属性指向构造函数

    如果构造函数中声明了和原型中一样的属性和方法,会优先访问实例对象中的属性和方法

    function Person(name,age){
      this.name = name;
      this.age=age;
      this.getName=function(){
        console.log('this is constuctor.');
    }     
          
    }
    
    Person.prototype.getName(){return this .name;};
    
    var p1=new Person('Tim',12);
    p1.getName();//this is constuctor.

    我们可以通过in判断一个对象是否拥有某个属性或方法不管这个属性或对象是不是挂载在原型对象上

    function Person(name,age){
      this.name = name;
      this.age=age;
      this.getName=function(){
        console.log('this is constuctor.');
    }     
          
    }
    
    Person.prototype.getName(){return this .name;};
    
    var p1=new Person('Tim',12);
    
    console.log('name' in p1);//true

    比较常用的用法是判断页面是否在移动端打开

    var isMobile='ontouchstart' in document;

    有多个原型可以这样写

    function Person(){}
    
    Person.prototype.getName(){}
    Person.prototype.getAge(){}
    ....

    还有更简单的写法,但是这种写法是给Prototype新建了一个原型对象,并不是原来的原型对象,需要显式指定constructor:Person

    function Person(){}
    
    Person.prtotype={
      constructor:Person,
      getName:function(){},
      getAge:function()(),
        ....      
    }

     五、原型链

    我们指定每个方法都有一个toString()方法,但是这个方法从哪里来的呢

    先声明一个函数

    function add() {}

    可以用下图来展示这个函数的原型链

    其中add是Function的实例,Function的原型又是Object的实例对象,这样就构成了一条原型链。通过__proto__属性,add可以访问原型链上面所有的属性和方法。

    六、继承

    一般我们需要结合构造函数和原型来创建一个对象,所以当需要继承时也要考虑构造函数和原型的继承。

    构造函数的继承

    构造函数的继承只需要使用call/apply把父级构造函数调用一下就可以了,这样子对象就具有了父对象在构造函数中定义的属性和方法

        function Person(name, age) {
            this.name = name;
            this.age = age;
        }
    
        Person.prototype.getName = function () { return this.name; };
        Person.prototype.getAge = function () { return this.age; };
    
        function Student(name, age, grade) {
            Person.call(this, name, age);
            this.grade = grade;
        }

    原型的继承需要把子对象的原型设置为父级的一个实例,加入原型链中

    // 继承原型
    Student.prototype = new Person(name, age);
    
    // 添加更多方法
    Student.prototype.getLive = function() {}

    更好的方法是定义一个空对象,让这个空对象的__proto__属性指向父级对象的原型,并封装成一个方法

        function create(proto, options) {
            var tmp = {};//声明一个空对象
            tmp.__proto__ = proto; //把临时对象的__proto__属性指向父类的原型对象,使得子类可以访问原型链上的所有方法和属性
            Object.defineProperties(tmp, options); //把需要公开的属性和方法挂载再子类的原型tmp上面
            return tmp;
            };
    
    
        Student.prototype = create(Person.prototype, {
            constructor: { value: Student },
            getGrade: { value: function () { return this.grade; } }
        });

    最后验证一下继承的正确性,完整代码

     function Person(name, age) {
            this.name = name;
            this.age = age;
        }
    
        Person.prototype.getName = function () { return this.name; };
        Person.prototype.getAge = function () { return this.age; };
    
        function Student(name, age, grade) {
            Person.call(this, name, age);
            this.grade = grade;
        }
    
        function create(proto, options) {
            var tmp = {};//声明一个空对象
            tmp.__proto__ = proto; //把临时对象的__proto__属性指向父类的原型对象,使得子类可以访问原型链上的所有方法和属性
            Object.defineProperties(tmp, options); //把需要公开的属性和方法挂载再子类的原型tmp上面
            return tmp;
            };
    
    
            Student.prototype = create(Person.prototype, {//除了使用自己封装的create方法可以用Object.create中现有的方法代替,和封装的方法是一样的
            constructor: { value: Student },
            getGrade: { value: function () { return this.grade; } }
        });
    
        var std1 = new Student("Nike", 13, 1);
        console.log(std1.getName()); //Nike
        console.log(std1.getAge()); //13
        console.log(std1.getGrade()); //1

    七、属性类型

    继承的时候用到了Object.defineProperties,这个方法可以用来设置属性类型。

    属性类型顾名思义就是属性的类型,直白点说就是属性的属性,用来描述属性的特定

    有几种属性类型,分别是:

    1)configurable:表示改属性是否能被delete,默认为true,如果为false,其他属性类型不能被改变;

    2)enumerable:是否能够枚举,即能否被for-in遍历,默认为true

    3)writable:是否能够修改值,默认为true;

    4)value:该属性的具体值是多少,默认undifined;

    5)get:当访问person.name的时候get方法被调用,该方法可以定义返回的具体值是多少,默认为undifined;

    6)set:当给person.name赋值时,set方法被调用,用于给name属性赋值,默认undifined;

    需要注意的是value,writable不能与get,set同时设置,否则乎报错,且getset应该成对设置,否则可能造成不能赋值或者不能设置值

    可以使用Object.difineProperty方法设置属性类型

    configurable

        var tom = { name: 'Tom' };
        //利用delete上传name属性,返回true表示成功
        console.log(delete tom.name); //true
    
        //使用Object.defineProperty设置name属性
        Object.defineProperty(tom, 'name', { value: 'jack', configurable: false });
    
        console.log(delete tom.name);//false,已经不能删除
        console.log(tom.name); //jack
        tom.name = "nik";//尝试改变name的值也不能修改
        console.log(tom.name); //还是jack

    enumerable

        var tom = { name: 'Tom',age:11 };
    
        var keys = [];
        for (var k in tom) {
            keys.push(k);
        }
    
        console.log(keys);//["name","age"]
    
        //使用Object.defineProperty设置name属性
        Object.defineProperty(tom, 'name', { enumerable: false });
    
        var key2 = [];
        for (var k in tom) {
            key2.push(k);
        }
        console.log(key2); //["age"]
    
        //注意enumerable:false是指不能通过for-in循环来判断name属性在不在tom对象中,并不是不能通过循环输出该属性的值,下面的语句依次输出tom,11
        ["name", "age"].forEach(function (item) { console.log(tom[item]) });

    writable

    var tom = { name: 'Tom',age:11 };
    
    
        console.log(tom.name); //Tom
        tom.name = "Jack";
        console.log(tom.name); //Jack,修改成功
        //使用Object.defineProperty设置name属性
        Object.defineProperty(tom, 'name', { writable: false });
        tom.name = "jone";
        console.log(tom.name); //Jack ,修改失败

    value

      var tom = {  };
    
        Object.defineProperty(tom, 'name', { value: 'tom' });
        console.log(tom.name); //tom

    get/set

     var tom = {  };
    
        Object.defineProperty(tom, 'name', {
            get: function () { return 'Tom'; },
            set: function (value) { console.log(  value + ' in set'); }
        });
        tom.name = 'jack';//jack in set
        console.log(tom.name);  //Tom

    上面是一次设置一个属性的属性类型,可以使用Object.defineProperties同时设置多个属性的属性类型

     var tom = {  };
    
        Object.defineProperties(tom, {
            name: {
                value: 'tom',
                configurable:false
            },
            age: {
                value: 12
            }
        });
    
        var descriptor = Object.getOwnPropertyDescriptor(tom, 'name'); //使用getOwnPropertyDescriptor获取某个属性的属性类型
        console.log(descriptor); //Object { value: "tom", writable: false, enumerable: false, configurable: false }

     

  • 相关阅读:
    FastAdmin 提示框 toastr 改变文字
    FastAdmin 将会员模块升级为基础模块的升级指导
    随笔
    随笔
    c++11 lambda(了解)
    c++11 类型推断
    boost::archive::text_oarchive
    std::ostringstream
    随笔
    随笔1
  • 原文地址:https://www.cnblogs.com/lidaying5/p/9026084.html
Copyright © 2020-2023  润新知