• JS模式和原型精解


    首先需要知道的是

    • 模式只是思想。不要用 结构 看模式。
    • ES中函数是对象,因此函数也有属性和方法。
    • 每个函数含有两个属性: length 和 prototype
    • 每个函数含有两个非继承的方法: apply 和 call

    属性中:最为重要的 prorotype 对应的值是一个 对象。 (length不重要,不管他了)

    • 就ES中的引用类型而言,prorotype 是他们所有实例方法的真正所在.
    • prototype 对象有一个 不可枚举的属性 constructor ,所以使用 for-in 无法发现。

    todo

    方法:每个函数包含两个非继承来的方法:call 和 apply

    • apply 接受两个参数。 函数名.apply(作用域, 参数[数组])

      举个例子。Math.max(param1, param2, ..)可以返回参数中的最大值。

      // 当我们想要对 数组使用,比较好的办法是使用 apply 方法
      const arr = [9, 91, 999];
      console.log(Math.max.apply(null, arr));
      
    • call 接受两个参数。 函数名.call(作用域, 参数a, 参数b, 参数...)

      function showColor() {
        console.log(this.color);
        return this.color;
      }
      function Test() {
        this.color = 'blue';
      }
      var o = {
        color: 'red'
      }
      var x = new Test();
      showColor.call(o);  // red - 此时作用域为 o的作用域
      showColor.call(x);  // blue - 此时作用域为 x的作用域
      

    再多唠叨一下,为了以后我看自己写的傻叉东西能够看懂。- 说下 new 一个函数的过程

    var x = new Person()

    • 1.创建一个空的 Object 对象。var obj = new Object
    • 2.将构造函数中的 this 指向刚创建的 obj对象。 (全局中直接调用/不使用new/函数this指向全局)
    • 3.将创建的 obj 的 __proto__ 指向构造函数 Person 的 prototype
    • 4.执行构造函数 Person 中的代码。

    1. 工厂模式

    工厂模式是 为了解决多个类似对象声明 的问题,为了解决实例化对象产生的重复。

    • 显示地创建了对象
    • 能够解决多个相似的问题
    • 无法识别对象
    function createObj(name, age, sex) {
      var obj = {};
      obj.name = name;
      obj.age = age;
      obj.sex = sex;
      obj.say = () => console.log('haha');
      return obj;
    }
    
    var x = createObj(1, 3, 4);
    var y = createObj(9, 9, 8);
    console.log(x.age, y.age); // 3, 9
    x.age = 12;
    x.say(); // haha
    console.log('x.say and y.say:', x.say === y.say); // false
    console.log(x, y);
    console.log(typeof x); // object
    console.log(x instanceof Object); // true
    

    2. 构造模式

    • 每个方法都要在每个实例上重新实现一遍(重复创建)
    • 可以用来区分 实例的 类型
    function Person(name, age, sex) {
      this.name = name;
      this.age = age;
      this.sex = sex;
      this.say = () => console.log('haha');
      // this.constructor = 'test';   // 把这句注释打开,我们的 constructor 就改变了。
    }
    
    var person1 = new Person(1, 3, 3);
    var person2 = new Person(3, 9 ,0);
    console.log(person1.say == person2.say); // false
    person1.say();
    
    console.log(person1.constructor === Person); // true
    console.log(person1 instanceof Person); // true
    console.log(person1 instanceof Object); // true 因为 所有对象都继承至 Object
    

    var person1 = new Person(1, 3, 3) 它经历了什么?

    1. 创建了一个新对象
    2. 将构造函数的作用域 赋值给新对象 (因此 this 就指向了 该对象)
    3. 执行构造函数内部的代码(为这个对象执行新的属性)
    4. 返回新对象

    上面若将注释打开,则利用constructor来判断将会出错,这就是 instanceof 的优点.

    总有人想问,不使用 new 是什么样子的。

    function Person(name, age) {
      this.name = name;
      this.age = age;
      // return 1;
    }
    const a = new Person('hah', 'pig');  // Person {name: 'hah', age: 'pig'}
    const b = Person(); // console.log(b) -> undefined  -- 因为函数本身没有返回 -- return注释打开则不同了
    const c = Person;   // console.log(b) -> [Function: Person]  --  将函数的地址 给 c,c相当于是函数的别名了 
    

    3. 原型模式

    每一个函数都有一个 prototype 属性,这个属性 是一个 指针,指向 调用构造函数而创建的那个对象实例的 原型对象。

    • 让所有对象共享他们的 属性 和 方法。
    • 缺点也是 所有的 属性和方法 都是 共享的。- 有些属性不需要共享。
    • 没有办法初始化参数。
    function Person() {
    }
    
    Person.prototype.name = 'chp';
    Person.prototype.age = 8;
    Person.prototype.say = function() {
      console.log(this.name);
    }
    
    var person1 = new Person();
    var person2 = new Person();
    
    person1.say();
    console.log(person1.say === person2.say); // true
     // 上面说明 没有多余地 创建 同样功能的函数,比起构造函数模式的优点。
    
    这里还需要注意一下两种写法的不同。
    function Cat() {}
    Cat.prototype.name = 'test';
    let cat = new Cat();
    console.log(cat instanceof Cat);     // true 
    console.log(cat.constructor === Cat);// true
    // -----------------------------------------------
    function Dog() {}
    Dog.prototype = {
      name: 'test'
    }
    let dog = new Dog();
    console.log(dog instanceof Dog);     // true 
    console.log(dog.constructor === Dog);// false  [Function: Object]
    

    [很关键]上面那样写实际上重写了原型的 constructor 为 Object

    有必要的话,我们得加上constructor:Dog,另外,这样也证明了 我们还是用 instanceof 比较保险, constructor 会被修改。

    function Person () {};
    Person.prototype = {
      constructor: 'Person'
    };
    console.log(Object.keys(Person.prototype)); // ['constructor'] - False,最好不被枚举出来
    console.log(Person.prototype.constructor); // Person - Right
    
    function PersonX () {};
    PersonX.prototype = {};
    console.log(Object.keys(PersonX.prototype)); // []
    console.log(PersonX.prototype.constructor); // [Function Object] - False,有必要最好是Person
    

    另外以这种方式重设 constructor,会导致该属性变为可枚举的。

    所以最完美的解决方式还要重设下 construcotr 的枚举属性

    function Person () {};
    Person.prototype = {
    };
    Object.defineProperty(Person.prototype, 'constructor', {
      value: Person,
      enumerable: false
    });
    console.log(Object.keys(Person.prototype));  // [] Right
    console.log(Person.prototype.constructor);   // [Function: Person]
    

    关于 prototype 和 constructor

    实例 f 的 constructor 指向了 构造函数 的 prototype 属性中的 constructor

    function F(){};
    console.log(F.prototype.constructor); // [Function: F]
    const f = new F();
    console.log(f.constructor);           // [Function: F]
    
    console.log(f.constructor === F.prototype.constructor); // true
    

    用实际程序来理解 高程三 中晦涩的原型对象。

    • 1.只要创建了一个函数,就会根据特定规则创建一个 prototype 属性,指向函数的原型对象。

      function F(){
        this.name = 'test';
      };
      console.log(F.prototype);  // F{} => 函数F 的 prototype 属性指向了 原型对象 F{}
      
    • 2.默认情况下,所有原型对象的 prototype 属性中的 constructor 属性都会指向自身。

      function F(){
        this.name = 'test';
      };
      console.log(F.prototype);  // F{}
      console.log(F);                       // [Function: F]
      console.log(F.prototype.constructor); // [Function: F]
      
    • 3.创建了自定义构造函数之后。其原型对象默认只会获得constructor属性。其他方法,都是从Object那里继承过来的。

      function F(){
        this.name = 'test';
      };
      console.log(F.prototype.constructor); // [Function: F]
      console.log(F.prototype.valueOf);     // [Function: valueOf]
      console.log(F.prototype.x);           // undefined
      
    • 为了理解上面一点,我们需要一点工具。 工欲善其事,必先利其器。

      更多关于工具的使用,请看最下方,这里为了不打断,只作简单介绍。

      • in 包含原型链-要求可访问(最多)
      • for-in 包含原型链-要求可访问、可枚举
      • Object.keys 只包含自身-要求可访问、可枚举(要求最高,所以最少)
      • Object.getPropertyNames 只包含自身-要求可访问(可访问到constructor)
    • 下面就来证实上面这一点:valueOf 来自于 原型链 而不是我们自定义的构造函数。

      function F(){};
      console.log(Object.getOwnPropertyNames(F.prototype));  // 只有一个: constructor 
      console.log(Object.getOwnPropertyNames(Object));  // ['length', 'name', 'argument', 'caller', 'create', 'prototype', ... ]
      console.log(Object.getOwnPropertyNames(Object.prototype)); // ['hasOwnProperty', 'constructor', 'toString', 'valueOf', '__proto__', ...]
      
    • 4.当调用构造函数创建一个新实例后,该实例的内部将包含一个指针__proto__(内部属性) ,指向构造函数的原型对象

      • 1.这个属性在 Node v6.10.0 上不可见,在很多实现上都不可见。但在Chorme高版本可见。

      • 2.这个连接 存在于 实例 与 构造函数的原型对象之间,而不是存在于实例与构造函数之间。

        • a.Chrome浏览器上我们可以看到 __proto__ 的实现
        • b.同样的代码写在 Node.js 中的效果
        • c.实例.__proto__构造函数.prototype
        • d.验证他们相等
    • 5.F的每个实例,都包含一个__proto__。实例本身虽然和构造函数没有直接的关系。但是和构造函数的原型却有直接的关系。虽然这两个实例都不包含 属性 和 方法。但我们却可以通过原型链查找来访问。

      F本身没有 valueOf 方法,但是可以通过 原型链查找,查找到 Object.prototype 里的方法.

      function F(){};
      const f = new F();
      console.log(f.valueOf()); // F {}
      
    • 6.另外 JS给我提供了 实例.__proto__ 是否指向 构造函数.ptototype(这个就是原型对象啦

      1. JS Object.prototype.isPrototypeOf()
      2. ES5 也新增了一个方法 Object.getPrototypeOf()
      function F() {};
      const f1 = new F();
      const f2 = new F();
      console.log(F.prototype.isPrototypeOf(f1)); // true 
      console.log(F.prototype.isPrototypeOf(f2)); // true
      console.log(Object.getPrototypeOf(f1))      // F {}
      console.log(Object.getPrototypeOf(f1) === F.prototype); // true
      
    • 7.原型链的搜索过程

      1. 实例 f 有 girlFriend 属性吗? 是的,没有
      2. 实例 f 的 原型(prototype) 有 girlFriend 属性吗? 有,上交给国家!
        这样就找到了。
    • 8.如果在 实例 中添加和 原型 中有的属性,实例中的属性 会覆盖 原型中的属性。

      function F() {};
      F.prototype.name = '123';
      let f1 = new F();
      let f2 = new F();
      f1.name = 'xx';
      console.log(f1.name);  // xx
      console.log(f2.name);  // 123
      console.log(f1.hasOwnProperty("name")); // true
      delete(f1.name);
      console.log(f1.name);  // 123
      console.log(f1.hasOwnProperty("name")); // false
      
    • 9.原型的动态性。我们对 原型对象 所做的任何修改都能立刻体现在 实例上。
      但是要注意,重写原型 和 给原型添加属性 是不一样的。

      function F(){};
      let f = new F();
      F.prototype.name = '123';
      console.log(f.__proto__.name);  // 123
      console.log(f.name);            // 123
      

      重写原型之后 切断了 原型与之前任何已经存在的 实例之间的 联系。他们引用的仍是最初的原型

      function F(){};
      let f = new F();
      F.prototype.name = 'ccc';
      F.prototype = {
        age: '20'
      }
      console.log(f.__proto__.name);  // ccc
      console.log(f.name);            // ccc
      console.log(f.__proto__.age);   // undefined
      console.log(f.age);             // undefined
      
    • 10.关于上面提到的另外一种 原型的写法, 我们可以 强行让 constructor 正确。但是这样写有一个缺点,那就是 constructor 变为 可枚举的类型了。因此,我们常常使用 ES5 的Object.defineProperty方法 来优化。

      function F() {};
      F.prototype = {
        constructor: 'F',
        name: '123'
      }
      const f = new F();
      console.log(f.constructor); // F
      

      再复习一次。

      function F() {};
      F.prototype = {
        name: '123'
      }
      Object.defineProperty(F.prototype, "constructor", {
        enumerable: false,
        value: F
      });
      const f = new F();
      console.log(Object.keys(F.prototype)); // ['name']
      

    由于属性方法 共享的 缘故, 原型的 主要缺点在于 实例的 独特性受到了限制。
    回忆一下

    • 缺点也是 所有的 属性和方法 都是 共享的。- 有些属性不需要共享。
    • 没有办法初始化参数。- 没有传参的地方

    4. 组合构造模式 : 集 构造函数模式 和 原型模式 优点于一身。·

    需要共享的属性和函数使用原型声明。不需要共享的和参数,都使用 构造函数来声明。

    function Pet(name, age, job) {
      this.name = name;
      this.age = age;
      this.job = job;
    }
    Pet.prototype.say = function() {
      console.log(this.age);
    }
    
    var cat = new Pet('cat', 3, '1');
    var dog = new Pet('dog', 4, '3');
    console.log(cat.say === dog.say);  // true
    cat.say();
    

    5. 动态原型模式 - 其实就是对组合构造模式的一点点优化而已。

    组合组合,明明一个类型,却要进行两个操作。动态原型模式 = 封装的 组合构造模式。

    function F(name) {
      this.name = name;
      if (typeof this.sayName != 'function') {
        F.prototype.sayName = () => console.log(this.name);
        F.prototype.sayHi = () => console.log('hi');
      }
    }
    let f = new F('hah');
    let x = new F('12');
    console.log(x.sayName == f.sayName); // true
    

    其中判断的作用主要是,只在 第一次 实例化前 进行相关 prototype 的设置。只设置一次。

    6. 寄生构造函数模式 - 其实就是工厂模式。硬要名字而已,无需特别在意。区别是使用了new。区别本身在于作用域,与模式无关。

    function F() {
      var o = new Object();
      console.log(this);
      o.name = '123';
      return o;
    }
    var a = new F(); //  F{}
    var b = F();     // 视环境而定,若环境为 Node 则为 Windows
    console.log(b.__proto__ == F.prototype); // false,跟工厂模式一样,返回的是内部的 Object
    

    该模式常用来 创造 基本引用类型 的 扩展。因为直接扩展到全局不太好。

    function optionArray() {
      var o = new Array();
      o.push.apply(o, arguments);
      o.toShow = function() {
        return this.join('/');
      }
      return o;
    }
    var x = new optionArray('little prince', 'red hat');
    console.log(x);
    console.log(x.toShow());
    

    其实,我们完全可以用其他的方法 创造一个类型。 而不是用它

    7. 稳妥构造函数,其实可以理解成 用 闭包 实现 C++ 的私有变量 和 公有方法

    • 没有公共属性(私有属性,公有方法---这就是C++)
    • 方法不引用 this 对象
    • 和寄生构造函数很像,区别在于 不使用 new
    function F(name) {
      var o = new Object();
      o.sayName = () => console.log(name);
      return o;
    }
    let f = F('nio');
    f.sayName(); // nio
    

    目的在于,除了 sayName() 函数自己,没有人能够找到 name.

    思想借鉴与 C++ 中的 private 和 public


    后记-复习-理解原型对象

    Person.protype 指向了 原型对象。
    原型对象 Person.protype.constructor 又指回了 Person
    console.log(Person.prototype.constructor); [function:Person]
    console.log(Person.prototype); // Person {name: 'chp', age: 8, say:[Function]}

    hasOwnProperty 和 in 和 for-in 和 Object.keys()

    function F() {
      this.name = 'csn'; // 构造函数中
    }
    F.prototype.sex = 'female'; // 原型中
    let f = new F();
    f.occupation = 'student';   // 实例中
    /*  --------------------------------------------- */
    console.log(f.hasOwnProperty("occupation")); // true, 自身实例有。
    console.log(f.hasOwnProperty("sex"));        // false, 在原型中
    console.log(f.hasOwnProperty("name"));       // true, 构造函数属于自身。
    console.log(F.prototype.hasOwnProperty("constructor")); // true (包含不可枚举的)
    /*  --------------------------------------------- */
    console.log("occupation" in f);  // true
    console.log("sex" in f);         // true
    console.log("name" in f);        // true
    console.log("constructor" in f); // true
    /*  --------------------------------------------- */
    for(let x in f) { console.log(x); } // name, occupation, sex (包含原型链,不包含不可枚举的)
    for(let x in F.prototype) {console.log(x);} // sex
    /*  --------------------------------------------- */
    console.log(Object.keys(f)); // ['name', 'occupation']   (不包含原型链,不包含不可枚举的)
    console.log(Object.keys(F.prototype)); // ['sex']
    

    in 搜索包括原型链上的 所有能访问(不管能不能枚举)的类型。而 hasOwnProperty 只是搜索本身的。

    person1.own = 'haha';
    console.log("own" in person1); // true
    console.log("age" in person1);  // true
    console.log(person1.hasOwnProperty("own")); // true
    console.log(person1.hasOwnProperty("age"));  // false
    

    in 和 for-in 的差别

    Object.defineProperty(person1, "own", {
      enumerable: false
    });
    console.log("own" in person1);   // true
    for(var property in person1) {
      console.log(property);         // name,age,say  
      // (已然没有了 own,它被设为了不可枚举)
    }
    

    Object.keys 和 Object.getOwnPropertyNames()

    • Object.keys 返回 本身所有可枚举的对象
    • Object.getOwnPropertyNames 返回 所有的对象,包括不可枚举的

    js里所有的函数都可以做构造函数,并且每个函数都有一个 prototype 属性,这个属性对应的值是一个对象,这个对象包含唯一 不可枚举的属性 constructor

    var person1 = new Person();
    person1.own = 1;
    console.log(Object.keys(person1));   // ['own']
    console.log(Object.keys(Person.prototype)); // ['name', 'age', 'say']
    console.log(Object.getOwnPropertyNames(person1)); // ['own']
    console.log(Object.getOwnPropertyNames(Person.prototype));
    // ['constructor', 'name', 'age', 'say']
    

    综上所述,每个玩意儿的要求不同

    方法名 特点 评价
    in 原型链和自身-所有 天罗地网,存在查找
    for-in 原型链和自身-可枚举 要求可枚举
    Object.keys 仅自身-可枚举 精确查找
    Object.getPropertyNames 仅自身-可枚举 仅自身的所有
    hasOwnProperty 仅自身-所有 同上
    • 属性不可枚举也能用的: in/hasOwnProperty.
    • 属性自身和原型链都能用的:in/for-in

    关于工厂模式、寄生构造模式、稳妥构造函数的区别

    • 稳妥构造函数: 不使用o.name = name这样的形式,拒绝被外部访问。用闭包。不使用 this/new 。拒绝属性直接访问。拒绝属性直接访问。拒绝属性直接访问。

    这是一种思想。

    • 寄生构造模式:目的是为了扩展一种新类型,又不影响全局的使用。使用 new。
    • 工厂模式。稳妥构造函数的 反面,不用 new。

    这里只写 工厂模式 和 稳妥构造函数 的 示例。

    function F1(name) {
      let o = new Object();
      o.name = name;
      return o;
    }
    function F2(name) {
      let o = new Object();
      o.getName = () => name;
      return o;
    }
    let f1 = F1('factory');
    let f2 = F2('safe');
    // 要想获取名字的话
    console.log(f1.name);   // factory
    console.log(f2.name);   // undefined
    console.log(f2.getName()); // safe
    

    complete.

  • 相关阅读:
    独立思考者模型:识别媒体与砖家的谎言 对精确性的痴迷
    独立思考者模型:你相信灵魂转世假说吗? 一次统计的头脑风暴
    独立思考者模型:如何分辨信息的真伪 不要再被虚假信息蒙骗
    独立思考者模型:寻找潜藏在表象背后的真相 探寻真相的方法
    独立思考者模型:避开思维误区的沼泽 我们很多时很蠢
    独立思考者模型:用专家的思维思考问题 从阅读到吸收再到模型建立
    独立思考者模型:如何科学地思考 掌握更正确的思维方式
    eclipse从svn检出项目
    网络通讯框架MINA和XSCOCKET的简单比较
    浏览器的渲染原理简介
  • 原文地址:https://www.cnblogs.com/can-i-do/p/8551788.html
Copyright © 2020-2023  润新知