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.

posted @ 2018-03-12 20:39  海客无心x  阅读(213)  评论(0编辑  收藏  举报