####原型设计模式
JavaScript是一种基于对象的语言, JavaScript中的所有对象, 都具有prototype属性。 prototype属性返回对象的所有属性和方法, 所有 JavaScript 内部对象都有只读的 prototype 属性, 可以向其原型中动态添加属性和方法, 但该对象不能被赋予不同的原型。 但是自定义的对象可以被赋给新的原型。
对象分为函数对象和普通对象, 区分: 凡是通过 new Function() 创建的对象都是函数对象, 其他的都是普通对象。 (Object , Function 是JS自带的函数对象)
1. 原型对象: prototype
在JS 中, 函数对象其中一个属性: 原型对象 prototype。 普通对象是没有prototype属性, 但有__proto__属性。
原型的作用就是给这个类的每一个对象都添加一个统一的方法, 在原型中定义的方法和属性都是被所以实例对象所共享。
例:
var person = function(name){
this.name = name
};
person.prototype.getName = function(){//通过person.prototype设置函数对象属性
return this.name;
}
var zxj= new person(‘zhangxiaojie’);
zxj.getName(); //zhangxiaojie //zxj继承上属性
2. 原型链
__proto__: js创建对象的内置属性, 用于指向创建它的函数对象的原型对象prototype。 (是JS内部使用寻找原型链的属性。 当实例化一个对象时候, 内部会新建一个__proto__属性并把prototype赋值给它)
下图为原型链运行图解, 当我们实例一个对象之后, 调用它的内部方法, 他的运行顺序是先找自身有没有该方法, 如果没有就会通过__proto__属性想上层寻找, 一直寻找到Object对象中, 如果还没有才会报错null
p._proto_----->Persion._proto_---->object._proto_----->null
3. 设计模式
(1)工厂模式: 在函数内创建一个对象, 给对象赋予属性及方法再将对象返回。
function Parent(){
var Child = new Object();
Child.name = "ZXJ";
Child.age = "24";
Child.sex=function(){
return "女";
};
return Child;
};
调用:
var x = Parent();
alert(x.name); //ZXJ
alert(x.sex()); //女
(2)构造函数模式:无需再函数内部重建创建对象, 而使用this指代
function Parent(){
var Child = new Object();
this.name = "ZXJ";
this.age = 24 ";
this.lev = lev;
Child.sex=function(){
return "女";
};
return Child;
};
调用:
var x = new Parent();
alert(x.name); //ZXJ
alert(x.sex()); //女
(3)原型模式: 函数中不对属性进行定义, 利用prototype属性对属性进行定义, 可以让所有对象实例共享它所包含的属性及方法。
function Parent(){ };
Parent.prototype.name = "ZXJ";
Parent.prototype.age = "24";
Parent.prototype.sex = function() {
var s="女";
alert(s);
};
调用:
var x = new Parent();
alert(x.name); //ZXJ
alert(x.sex()); //女
(4)混合模式: 原型模式+构造函数模式。 这种模式中, 构造函数模式用于定义实例属性, 而原型模式用于定义方法和共享属性。
function Parent(){
this.name = "ZXJ";
this.age = 24;
};
Parent.prototype.sayname=function(){
return this.name;
};
调用:
var x =new Parent();
alert(x.sayname()); //ZXJ
(5)动态原型模式:将所有信息封装在了构造函数中, 而通过构造函数中初始化原型, 这个可以通过判断该方法是否有效而选择是否需要初始化原型。
function Parent(){
this.name = "ZXJ";
this.age = 24;
if (typeof Parent._sayname == "undefined") {
Parent.prototype.sayname = function() {
return this.name;
}
Parent._sayname = true;
}
};
调用:
var x = new Parent();
alert(x.sayname());
####原型规则
A. 所有的引用类型(数组、 对象、 函数), 都具有对象特性, 即可自由扩展属性;
var arr = [];
arr.a = 1;
B. 所有的引用类型(数组、 对象、 函数), 都有一个_proto_属性(隐式原型), 属性值是一个普通的对象;
C. 所有的函数, 都具有一个prototype(显式原型), 属性值也是一个普通对象;
D. 所有的引用类型(数组、 对象、 函数), 其隐式原型指向其构造函数的显式原型; (obj._proto_ === Object.prototype);
E. 当试图得到一个对象的某个属性时, 如果这个对象本身没有这个属性, 那么会去它的_proto_(即它的构造函数的prototype)中去寻找;
####instanceof底层是如何工作的:
function instance_of(L, R) {//L 表示左表达式, R 表示右表达式
var O = R.prototype; // 取 R 的显示原型
L = L.__proto__; // 取 L 的隐式原型
while (true) {
if (L === null)
return false;
if (O === L) // 当 O 显式原型 严格等于 L隐式原型 时,返回true
return true;
L = L.__proto__;
}
}
####手动实现一个instanceof (用到constructor)
function instance_of(L, R) {//L 表示左表达式, R 表示右表达式
var O = R;
L = L.__proto__;
while (true) {
if (L === null)
return false;
if (O === L.constructor) // 这里重点: 当 O 严格等于 L 时, 返回 true
return true;
L = L.__proto__;
}
}
// 开始测试
var a = []
var b = {}
function Foo(){}
var c = new Foo()
function child(){}
function father(){}
child.prototype = new father()
var d = new child()
console.log(instance_of(a, Array)) // true
console.log(instance_of(b, Object)) // true
console.log(instance_of(b, Array)) // false
console.log(instance_of(a, Object)) // true
console.log(instance_of(c, Foo)) // true
console.log(instance_of(d, child)) // false 这里就是无法用于判断继承的
console.log(instance_of(d, father)) // true
####4. 实现继承的几种方式以及他们的优缺点
见: 理解原型设计模式以及JavaScript中的原型规则
####优缺点:
1. 原型链继承 -》缺点: 构造函数原型上的属性在所有该构造函数构造的实例上是共享的, 即属性没有私有化, 原型上属性的改变会作用到所有的实例上。
2. 构造函数继承 -》优缺点: 实现了属性的私有化, 但是子类无法访问父类原型上的属性。
3. 组合继承 -》 利用构造函数和原型链的方法, 可以比较完美的实现继承
function Super(){
this.flag = true;
}
Super.prototype.getFlag = function(){
return this.flag; //继承方法
}
function Sub(){
this.subFlag = flase
Super.call(this) //继承属性
}
Sub.prototype = new Super;
var obj = new Sub();
Sub.prototype.constructor = Sub;
Super.prototype.getSubFlag = function(){
return this.flag;
}
注:
这里还有个小问题, Sub.prototype = new Super; 会导致Sub.prototype的constructor指向Super; 然而constructor的定义是要指向原型属性对应的构造函数的, Sub.prototype是Sub构造函数的原型, 所以应该添加一句纠正: Sub.prototype.constructor = Sub;
4. 寄生继承 -》 即将sub.prototype=new super改为sub.prototype=Object.creat(supper.prototype), 避免了组合继承中构造函数调用了两次的弊端。
不理解
####述new一个对象的详细过程:
使用new关键字调用函数(new ClassA(…))的具体步骤:
1. 创建空对象;
var obj = {};
2. 设置新对象的constructor属性为构造函数的名称, 设置新对象的__proto__属性指向构造函数的prototype对象;
obj.__proto__ = ClassA.prototype;
3. 使用新对象调用函数, 函数中的this被指向新实例对象:
ClassA.call(obj); //{}. 构造函数();
4. 将初始化完毕的新对象地址, 保存到等号左边的变量中
注意: 若构造函数中返回this或返回值是基本类型(number、 string、 boolean、 null、 undefined)的值, 则返回新实例对象; 若返回值是引用类型的值, 则实际返回值为这个引用类型。
var foo = "bar";
function test () {
this.foo = "foo";
}
new test(); //test中的this指新对象, 并未改变全局的foo属性
console.log(this.foo); // "bar"
console.log(new test().foo); // "foo";
####手动实现一个new操作符:
第一种方法:
function new1(func) {
// 创建一个继承自func.prototype的新对象
var newObj = Object.create(func.prototype);
//截取new1函数第二个以及第二个之后的参数,在newObj作用域内执行改造函数func
var returnObj = func.apply(newObj, Array.prototype.slice.call(arguments, 1));
//如果传入参数中的构造函数执行后的returnObj是“对象”类型(比如new1(Object)),那么这个对象会取代newObj作为返回的对象
if ((typeof returnObj === "object" || typeof returnObj === "function") && ret !== null) {
return returnObj;
}
return newObj;
}
第二种方法:
function new2(func) {
return function() {
// 新生成一个对象,且新对象的原型对象继承自构造对象的原型对象
let newObj = {
__proto__: func.prototype
}
// 以第二次执行函数的参数,在obj作用域中执行func
var returnObj = func.apply(obj, arguments)
//同理,returnObj是“对象”类型(比如new1(Object)),那么这个对象会取代newObj作为返回的对象
if ((typeof returnObj === "object" || typeof returnObj === "function") && returnObj !== null) {
return returnObj;
}
return newObj
}
}
第三种方法:
function objectFactory() {
// 通过arguments类数组打印出的结果,我们可以看到其中包含了构造函数以及我们调用objectfactory时传入的其他参数
// 由于arguments是类数组,没有直接的方法可以供其使用,我们可以有以下两种方法:
// 1. Array.from(arguments).shift(); //转换成数组 使用数组的方法shift将第一项弹出
// 2.[].shift().call(arguments); // 通过call() 让arguments能够借用shift方法
let Constructor = [].shift.call(arguments);
const obj = new Object();
obj.__proto__ = Conctructor.prototype;
// 接下来的想法 给obj这个新生对象的原型指向它的构造函数的原型
// 给构造函数传入属性,注意:构造函数的this属性
// 参数传进Constructor对obj的属性赋值,this要指向obj对象
// 在Coustructor内部手动指定函数执行时的this 使用call、apply实现
Constructor.call(obj, ...arguments);
return obj;
}
####class构造:
Class本质上是一个特别的函数
Class 在语法上更加贴合面向对象的写法
Class 实现继承更加易读、 易理解
更易于写 java 等后端语言的使用
本质还是语法糖, 使用 prototype
es5:
function Fun(a, b) {
this.a = a;
this.b = b;
}
Fun.prototype.showA = function () {
console.log(this.a)
}
var fun = new Fun(1, 2);
fun.showA(); //1
es6则引用了class的概念, 使得更接近java、 c++等语言, 更加直观。 如:
class Fun {
constructor(a, b) {
this.a = a;
this.b = b;
}
showA() {
console.log(this.a);
}
}
var fun = new Fun(1, 2);
fun.showA(); //1
这两种写法是一样的, 在es6中, class可以理解为一个语法糖, 只是让这种写法更加直观。
要注意的是, es6中声明新的实例必须要用new声明。
其中constructor为类的默认方法, 通过new的调用可以执行这个方法。 每个类都必须要有这个方法, 如果没有显示定义,
则一个空的constructor被添加到类里面。 constructor方法默认返回实例对象, 即this。 也可以返回其他对象。 这事, 新的实例instanceof当前class就会报错。
####class继承的底层实现原理:
下面我们借助babel来探究es6类和继承的实现原理。
#####1. 类的实现
转换前:
class Parent {
constructor(a){
this.filed1 = a;
}
filed2 = 2;
func1 = function(){}
}
转换后:
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Parent = function Parent(a) {
_classCallCheck(this, Parent);
this.filed2 = 2;
this.func1 = function () { };
this.filed1 = a;
};
解析:
1. 调用_classCallCheck方法判断当前函数调用前是否有new关键字。
构造函数执行前有new关键字, 会在构造函数内部创建一个空对象, 将构造函数的proptype指向这个空对象的_proto_, 并将this指向这个空对象。
如上, _classCallCheck中: this instanceof Parent 返回true。
若构造函数前面没有new则构造函数的proptype不会不出现在this的原型链上, 返回false。
2. 将class内部的变量和函数赋给this。
3. 执行constuctor内部的逻辑。
4.return this (构造函数默认在最后我们做了)。
######2. 继承实现
转换前:
class Child extends Parent {
constructor(a, b) {
super(a);
this.filed3 = b;
}
filed4 = 1;
func2 = function(){}
}
转换后:
function _inherits(subClass, superClass) {
// (1) 校验父构造函数。
// (2) 典型的寄生继承:用父类构造函数的proptype创建一个空对象,并将这个对象指向子类构造函数的proptype。
// (3) 将父构造函数指向子构造函数的_proto_(这步是做什么的不太明确,感觉没什么意义。)
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass){
Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
}
function _possibleConstructorReturn(self, call) {
// 这里的Child.proto || Object.getPrototypeOf(Child)实际上是父构造函数(_inherits最后的操作),
// 然后通过call将其调用方改为当前this, 并传递参数。 (这里感觉可以直接用参数传过来的Parent)
// 校验this是否被初始化, super是否调用, 并返回父类已经赋值完的this。
if (!self) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return call && (typeof call === "object" || typeof call === "function") ? call : self;
}
var Child = function (_Parent) {
_inherits(Child, _Parent);
function Child(a, b) {
_classCallCheck(this, Child);
var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, a));
_this.filed4 = 1;
_this.func2 = function() {};
_this.filed3 = b;
return _this;
}
return Child;
}(Parent);
以上代码做了:
1. 调用_inherits函数继承父类的proptype。
2. 用一个闭包保存父类引用, 在闭包内部做子类构造逻辑。
3. new检查。
4. 用当前this调用父类构造函数。
5. 将行子类class内部的变量和函数赋给this。
6. 执行子类constuctor内部的逻辑。
可见, es6实际上是为我们提供了一个“组合寄生继承”的简单写法。