ES6 函数与对象(三)

函数与对象

JS中的面向对象设计和面向委托的设计

很多开发者会尝试在JS中使用面向对象设计,而JS中最符合它风格的编程方式其实是对象关联编程方式,即面向委托的设计

0. [[GET]] [[PUT]]和[[Prototype]]

  • [[GET]]
    属性访问时,实际上进行了[[GET]]操作,例如obj.a,大致算法如下
if obj has a
   if a has accessor property
       if a has getter
           do getter
       else
           reject
   else
       get value
else 
   do [[Prototype]] 
  • [[PUT]]

属性赋值时,实际上进行了[[PUT]]操作,例如obj.a=1,大致算法如下:

if obj has a
   if a has accessor property
       if a has setter
           do setter
       else reject
   else if a is not writable
       reject or throw TypeError
   else 
       set value
else
   do [[Prototype]] 
  • [[Prototype]]

js对象有一个特殊的[[Prototype]]属性,它其实是对其他对象的引用。在[[GET]]操作中,如果当前对象没有属性值,它会沿着这个对象的[[Prototype]]属性查找关联对象中是否有这个属性,直到找到Object.prototype,所有普通对象其实都最终指向内置的Object.prototype

下层属性会屏蔽上层同名属性,不过也有很多种情况,比如obj.a=1

if obj.[[Prototype]] has a
   if a in [[Prototype]] is writable
       a add to obj---------------------1
   else if a is a setter
       do setter(a will not add to obj)-2
   else
       do nothing or throw TypeError----3
else
   do [[Prototype]] 

情况1当然是最容易理解的

情况2如下:

var a = {
   _x : 1,
   set a(v) {
       this._x = v;
       console.log("set");
   }, 
   get a(){
       return this._x;
   }
};
function b(){};
b.prototype = a;
var c = new b();
c.a = 2;
// set
c.hasOwnProperty(a);
// false 

情况3如下:

var c = {x:1};
Object.defineProperty(c, "x", {writable:false});
var d = Object.create(c);
d.x = 2;
d.x
// 1 

下面给出一个创建函数,以及通过函数,创建一个对象的原型链图

function a() {}
var b = new a(); 

1. 在JS中模拟面向对象

  • 类与构造函数
function A(){}
//以上a是普通函数
// 现在在new的后面,它化身为构造函数
var b = new A(); 
  • 继承与多态

在js中我们可以通过原型来模拟继承

模拟过程如下:

function A(){}
function B(){}
B.prototype = Object.create(B.prototype);
var c = new B(); 

我们的可以对上面的原型链图加以修改给出以下链图

从以上链图中我们可以看到Function和Object的关系十分微妙,实在无法想出他们当初怎么会设计出这个模型,如此巧妙

我们要注意以下写法

function A() {}
function B() {}
B.prototype = new A(); 

上面的方法可以完成B继承A的任务,但是却执行A函数中内容,这是有点危险的,而且B构造的对象能够A中私有变量进行访问。

其实上述方法都不能说是继承,只能说模拟继承,继承的本质是复制,子类复制父类的方法

在Javascript中我们可以通过混入来模拟类的赋值行为

  1. 显式混入
function mixin(sourceObj, targetObj) {
   for(var key in sourceObj) {
       if(!(key in targetObj)) {
           targetObj[key] = sourceObj[key];
       }
   }
   return targetObj;
}


var A = {
   name:"a",
   say: function(){
       return this.name;
   }
};


var B = mixin(A, {
   name: "b"
});

B.say();
// b 

通过以上方法,可以简单的将父类的属性复制到子类中,这个称为显式混入

  1. 寄生继承
function A(){
   this.name = "a";
}

A.prototype.say = function() {
   return this.name;
}

function B(){
   var a = new A();
   // 进行定制
   a.name = "b";
   var asay = a.say;
   a.say = function(){
       return "say"+asay.call(this);
   }
   return a;
}

var b = new B();
b.say();
// sayb 
  1. 隐形混入
var A = {
   name: "a",
   say: function(){
       return "hello, I am " + this.name;
   }
};

var B = {
   name: "b",
   say: function(){
       return A.say.call(this);
   }
}
B.say();
hello, I am b 

多态指的是在继承链上,不同类中名字相同的函数具有不同功能

其实我们上述所讲的模拟继承的原型链,以及混入其实都有不同程度地体现多态的思想

  • 类方法,类字段,实例方法,实例字段

以原型模拟的继承为例

function A() {
   
}
A.name = "A"; // 类字段
A.say = function(){} // 类方法
A.prototype.name = "A"; // 实例字段
A.prototype.say = function(){
   return this.name;
} // 实例方法 
  • 变量封装

Javascript主要通过闭包来实现变量的封装

var A = (function A(){
   var _x = 1;
   return {
     y:1,
     getX: function(){
         return _x;
     },
     setX: function(v){
         _x = v;
     }
   };
}());
A.setX(2);
A.getX();
// 2 

通过闭包我们完成了对私有变量_x的封装

  • 类型检查

在面向对象中,类型检测十分重要,下面是一个检查构造函数名称的方法:

function type(o) {
 var t, c, n;
 if(o === null) return "null";
 if(o !== o) return "nan";
 // 如果是原始值,使用typeof判断
 if((t = typeof o) !== "object") return t;
 // 如果是内置对象,可以使用toString方法
 if((c = getClass(o)) !== "object" ) return c;
 if(o.constructor 
   && typeof o.constructor === "function" 
   && (n = o.constructor.getName())) {
   return n;
 }
 return "object";
}

function getClass(object) {
 return Object.prototype.toString.call(object).match(/\[object\s+(\w+)\]/)[1];
}
Function.prototype.getName = function() {
 if("name" in this) return this.name;
 return this.name = this.toString().match(/function\s*([^(]*)\(/)[1];
} 

还有instanceof可以检测对象的构造函数,它回答的问题是在对象的整条[[prototype]]链中是否有有构造函数的prototype指向的对象

还有一个方法isPrototypeof

Foo.prototype.isPrototypeOf(a) 

它则是回答在a的整条原型链中是否出现过Foo.prototype,本质上与instanceof类似。

2. 使用面向委托的设计

[[prototype]]机制是指对象中的一个内部链接引用另一个对象。其本质是对象之间的关联关系

而委托行为意味着在对象找不到属性或者方法引用时,会将这个请求委托给另一个对象。与面向类设计那种从父类到子类的垂直组织不同,面向委托设计可以以任意方向并排组织。不过要注意禁止相互委托,即A委托B,而B又委托A,这样会造成循环委托

我们来写一个实际例子比较一个面向对象和面向委托设计的不同

面向对象设计

function Auth() {}

Auth.prototype.auth = function(name, passwd) {
   console.log("name is " + name + " password is " + passwd);
}

function Login() {}
Login.prototype.login = function(){
   var auth = new Auth();
   auth.auth("jeff", "123456");
};

var login = new Login()
login.login(); 

面向委托的设计

var Auth = {
  auth: function(name, passwd) {
       console.log("name is " + name + " password is " + passwd);
  } 
};

// 将登录的一些行为委托给Auth
var Login = Object.create(Auth);
Login.login = function(){
   this.auth("jeff", "123456");
}

Login.login(); 

功能相同,而且如果能够理解行为委托的设计风格的话,会发现这种写法十分简洁。

Auth和Login并不是父子关系,继承难以理解,面向对象只能通过组合模式来完成两者的协作工作,然而行为委托可以把两者看成是任意的关系,十分自由

待续