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)
它经历了什么?
- 创建了一个新对象
- 将构造函数的作用域 赋值给新对象 (因此 this 就指向了 该对象)
- 执行构造函数内部的代码(为这个对象执行新的属性)
- 返回新对象
上面若将注释打开,则利用
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.验证他们相等
- a.Chrome浏览器上我们可以看到
-
-
5.F的每个实例,都包含一个
__proto__
。实例本身虽然和构造函数没有直接的关系。但是和构造函数的原型却有直接的关系。虽然这两个实例都不包含 属性 和 方法。但我们却可以通过原型链查找来访问。F本身没有
valueOf
方法,但是可以通过 原型链查找,查找到 Object.prototype 里的方法.function F(){}; const f = new F(); console.log(f.valueOf()); // F {}
-
6.另外 JS给我提供了
实例.__proto__
是否指向构造函数.ptototype
(这个就是原型对象啦- JS
Object.prototype.isPrototypeOf()
- 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
- JS
-
7.原型链的搜索过程
- 实例 f 有 girlFriend 属性吗? 是的,没有
- 实例 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.