• 04面向对象编程-01-创建对象 和 原型理解(prototype、__proto__)


    1、JS中对象的”不同”:原型概念

    从Java中我们可以很好地去理解 “类” 和 “实例” 两个概念,可是在JavaScript中,这个概念却不一样。

    JavaScript不区分类和实例的概念,而是通过原型(prototype)来实现面向对象编程。

    原型是指当我们想要创建一个具体对象时,并没有像Java中那样有类可以使用,但是却可以利用类似“继承”的方式,这里类似“父类”的对象,就是所谓的原型。(再简单点:原型也是一个对象,通过原型可以实现对象的属性继承

    比如:我们有一个Student对象,现在我们通过它作为原型,创建出xiaoming:
    var Student = { 
        name: 'Robot',
        height: 1.2,
        run: function () {
            console.log(this.name + ' is running...');
        }
    };
    
    var xiaoming = {
        name: '小明'
    };
    
    xiaoming.__proto__ = Student; //xiaoming的原型指向了对象Student

    这里的xiaoming有自己的name,但是没有定义run()方法,不过没关系,他是“继承“Student,所以只要Student有run()方法,xiaoming也就可以直接使用。

    Object.create() 方法可以传入一个原型对象,并创建一个基于该原型的新对象,但这个新对象什么属性也没有。基于以上,我们也可以编写一个函数来创建xiaoming:
    // 原型对象:
    var Student = {
        name: 'Robot',
        height: 1.2,
        run: function () {
            console.log(this.name + ' is running...');
        }
    };
    
    function createStudent(name) {
        // 基于Student原型创建一个新对象:
        var s = Object.create(Student);
        // 初始化新对象:
        s.name = name;
        return s;
    }
    
    var xiaoming = createStudent('小明');
    xiaoming.run(); // 小明 is running...
    xiaoming.__proto__ === Student; // true

    2、创建对象 和 原型的理解

    当我们使用 obj.xxx 访问一个对象的属性时,这个过程和Java中访问属性极为相似,都是顺着继承链不断往上搜寻。JavaScript引擎会先在当前对象查找,没找到就到其原型对象上找,一直如此直到追溯到 Object.prototype 对象,最后如果还没有找到,就只能返回 undefined。

    比如我们有Array对象 var arr = [1, 2, 3]; 其原型链是 arr --> Array.prototype --> Object.prototype --> null,indexOf()等方法都是在Array.prototype中定义的,所以你可以在所有的Array对象上直接调用这些方法。

    创建对象的方式:
    • 使用 { ... } 创建
    • 使用 new 构造函数

    function Student(name) {
        this.name = name;
        this.hello = function () {
            alert('Hello, ' + this.name + '!');
        }
    }
    //这确实是一个普通函数,如果不写new的话;
    //如果写了new,它就是一个构造函数,默认返回this,不需要在最后写return this;
    
    var xiaoming = new Student('小明');
    xiaoming.name; // '小明'
    xiaoming.hello(); // Hello, 小明!
    
    //这里的xiaoming原型就指向这个构造函数Student了

    new 的方式创建的对象,还从原型上获得了一个 constructor 属性,这个属性指向构造函数本身
    xiaoming.constructor === Student.prototype.constructor; // true
    Student.prototype.constructor === Student; // true
    
    Object.getPrototypeOf(xiaoming) === Student.prototype; // true
    
    xiaoming instanceof Student; // true

    好了好了,到这里就有点乱,什么constructor、prototype、__proto__ 等等,我们来理理概念先:
    • constructor 属性,指向对象的构造函数,每个对象都有
    • prototype 属性,是可以作为构造函数的函数对象,才具备的属性,比如这里的Student
    • __proto__ 属性是任何对象(除了null)都具备的属性
    • 上面两者的指向,都是其所属的原型对象。所以为什么构造函数有了__proto__还要有一个同样的prototype呢,当一个函数被用作构造函数来创建实例时,该函数的prototype属性值将被作为原型赋值给所有对象实例(即设置实例的__proto__属性)

    所以上面我们提到的Student,我们输出它的prototype看看:

    再看看小明的__proto__,很明显和Student.prototype是一样的,毕竟是赋值过来的:

    可以看到,它们的原型对象有两个属性:constructor 和 __proto__ :
    • 前者指向构造函数Student(),相当于是标记自己是和Student这个构造函数发生联系的;
    • 后者指向是这个原型对象的原型对象,这里是Object

    廖老师的原文中用了一张图来解释这些关系:
    你可以把构造函数想象成女人A,某个实例对象是男人B(这个实例对象由其他构造函数产生),女人通过prototype属性指向男人,男人通过constructor属性指向女人,相互指向以后就相当于结婚仪式了,这下好了是夫妻了。

    女人生下孩子时,就通过把prototype赋值给孩子的__proto__属性,相当于告诉他们让他们知道自己的父亲是谁。既然这些孩子知道父亲是谁了,正所谓 “龙生龙,凤生凤,老鼠的儿子会打洞”,这些孩子虽然本来没有学过什么技能,但是也可以通过他们的老爹继承,自然而然地会使用一些技能了。

    所以,记住这句话,实际上,原型继承的本质是 “将构造函数的原型对象,指向由另一个构造函数创建的实例

    以上,我们可以再看看Student的 prototype 和 __proto__:

    我们可以看到:
    • prototype指向的原型对象,也就是xiaoming的老爹,是个没什么多余属性的几乎空白的一个对象,所以xiaoming没得到什么新能力
    • 构造函数的原型对象(就是它爹)是一个空的函数 function( ) { } 

    看到这里,再结合之前的一个示例代码,我们稍微改动一下,当小明发现他的爹实际上是隔壁老王:
    var laoWang = {
        name: '隔壁老王',
        height: 1.2,
        run: function () {
            console.log(this.name + ' is running...');
        }
    };
    
    var xiaoming = {
        name: '小明'
    };
    
    xiaoming.__proto__ = laoWang; //xiaoming的原型指向了对象Student //相当于告诉小明的爹是谁,比如小明的爹变成了老王
    
    xiaoming.run(); // 小明 is running...  //小明没定义run方法也会使用run()了

    需要注意的是,方法定义在构造函数中比如run定义在构造函数中,多个对象虽然都有run但是run方法是不同的,用 “===” 比较会得到 false;实际上他们可以共享同一个函数,把这个函数移动到原型对象里就好了,那么多个对象的方法相当于是继承的,而不是构造函数定义的,用 "===" 比较就会得到true。
    function Student(name){
      this.name = name;
      this.run = function(){return this.name + "is running..."}
    }
    
    var xiaoming = new Student("xiaoming");
    var xiaohong = new Student("xiaohong");
    
    xiaoming.run === xiaohong.run;
    //false
    
    ----------------------------------------------------------------------
    function Student(name){
      this.name = name;
    }
    
    Student.prototype.run = function(){return this.name + "is running..."}
    
    xiaoming = new Student("xiaoming");
    xiaohong = new Student("xiaohong");
    
    xiaoming.run === xiaohong.run;
    //true

    3、其他的话

    按照约定,构造函数的首字母要大写,普通函数的首字母才小写,用以区分。

    调用构造函数的时候千万不要忘记写 new,否则函数作为普通函数被运行,this 会被绑定到全局对象window,无意间你就创建了一个全局变量。

    我们可以写一个 createXXX() 函数,把 new 的操作封装进去:
    function Student(props) {
        this.name = props.name || '匿名'; // 默认值为'匿名'
        this.grade = props.grade || 1; // 默认值为1
    }
    
    Student.prototype.hello = function () {
        alert('Hello, ' + this.name + '!');
    };
    
    function createStudent(props) {
        return new Student(props || {})
    }

    这种方式不会遗漏new,参数也很灵活,可以不传,也可以传一个对象:
    var xiaoming = createStudent({
        name: '小明'
    });
    
    xiaoming.grade; // 1

    如果创建的对象有很多属性,我们只需要传递需要的某些属性,剩下的属性可以用默认值。由于参数是一个Object,我们无需记忆参数的顺序。如果恰好从JSON拿到了一个对象,就可以直接创建出xiaoming。


    除了廖老师的教程参考,其他参考链接:

  • 相关阅读:
    mac系统怎么给文件夹加密?mac文件夹加密教程
    android 代码优化:关闭输出日志
    mac下安装eclipse+CDT
    Android JNI MAC OS环境配置
    在mac下设置环境变量
    build_native.py文件分析(2)
    使Android 自带SDK 完美支持HTML5 之 html5webview
    WebView基本使用
    android EditText如何使光标随着输入内容移动
    mac下增加eclipse内存
  • 原文地址:https://www.cnblogs.com/deng-cc/p/6642427.html
Copyright © 2020-2023  润新知