澳门新萄京官方网站-www.8455.com-澳门新萄京赌场网址

深远理解,浓密理解Javascript面向对象编制程序

2019-07-14 作者:澳门新萄京赌场网址   |   浏览(172)

深入理解Javascript面向对象编程

2015/12/23 · JavaScript · 1 评论 · 面向对象

原文出处: 涂根华   

一:理解构造函数原型(prototype)机制

prototype是javascript实现与管理继承的一种机制,也是面向对象的设计思想.构造函数的原型存储着引用对象的一个指针,该指针指向与一个原型对象,对象内部存储着函数的原始属性和方法;我们可以借助prototype属性,可以访问原型内部的属性和方法。

当构造函数被实列化后,所有的实例对象都可以访问构造函数的原型成员,如果在原型中声明一个成员,所有的实列方法都可以共享它,比如如下代码:

JavaScript

// 构造函数A 它的原型有一个getName方法 function A(name){ this.name = name; } A.prototype.getName = function(){ return this.name; } // 实列化2次后 该2个实列都有原型getName方法;如下代码 var instance1 = new A("longen1"); var instance2 = new A("longen2"); console.log(instance1.getName()); //longen1 console.log(instance2.getName()); // longen2

1
2
3
4
5
6
7
8
9
10
11
12
// 构造函数A 它的原型有一个getName方法
function A(name){
    this.name = name;
}
A.prototype.getName = function(){
    return this.name;
}
// 实列化2次后 该2个实列都有原型getName方法;如下代码
var instance1 = new A("longen1");
var instance2 = new A("longen2");
console.log(instance1.getName()); //longen1
console.log(instance2.getName()); // longen2

原型具有普通对象结构,可以将任何普通对象设置为原型对象; 一般情况下,对象都继承与Object,也可以理解Object是所有对象的超类,Object是没有原型的,而构造函数拥有原型,因此实列化的对象也是Object的实列,如下代码:

JavaScript

// 实列化对象是构造函数的实列 console.log(instance1 instanceof A); //true console.log(instance2 instanceof A); // true // 实列化对象也是Object的实列 console.log(instance1 instanceof Object); //true console.log(instance2 instanceof Object); //true //Object 对象是所有对象的超类,因此构造函数也是Object的实列 console.log(A instanceof Object); // true // 但是实列化对象 不是Function对象的实列 如下代码 console.log(instance1 instanceof Function); // false console.log(instance2 instanceof Function); // false // 但是Object与Function有关系 如下代码说明 console.log(Function instanceof Object); // true console.log(Object instanceof Function); // true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 实列化对象是构造函数的实列
console.log(instance1 instanceof A); //true
console.log(instance2 instanceof A); // true
 
// 实列化对象也是Object的实列
console.log(instance1 instanceof Object); //true
console.log(instance2 instanceof Object); //true
 
//Object 对象是所有对象的超类,因此构造函数也是Object的实列
console.log(A instanceof Object); // true
 
// 但是实列化对象 不是Function对象的实列 如下代码
console.log(instance1 instanceof Function); // false
console.log(instance2 instanceof Function); // false
 
// 但是Object与Function有关系 如下代码说明
console.log(Function instanceof Object);  // true
console.log(Object instanceof Function);  // true

如上代码,Function是Object的实列,也可以是Object也是Function的实列;他们是2个不同的构造器,我们继续看如下代码:

JavaScript

var f = new Function(); var o = new Object(); console.log("------------"); console.log(f instanceof Function); //true console.log(o instanceof Function); // false console.log(f instanceof Object); // true console.log(o instanceof Object); // true

1
2
3
4
5
6
7
var f = new Function();
var o = new Object();
console.log("------------");
console.log(f instanceof Function);  //true
console.log(o instanceof Function);  // false
console.log(f instanceof Object);    // true
console.log(o instanceof Object);   // true

我们明白,在原型上增加成员属性或者方法的话,它被所有的实列化对象所共享属性和方法,但是如果实列化对象有和原型相同的成员成员名字的话,那么它取到的成员是本实列化对象,如果本实列对象中没有的话,那么它会到原型中去查找该成员,如果原型找到就返回,否则的会返回undefined,如下代码测试

JavaScript

function B(){ this.name = "longen2"; } B.prototype.name = "AA"; B.prototype.getName = function(){ return this.name; }; var b1 = new B(); // 在本实列查找,找到就返回,否则到原型查找 console.log(b1.name); // longen2 // 在本实列没有找到该方法,就到原型去查找 console.log(b1.getName());//longen2 // 如果在本实列没有找到的话,到原型上查找也没有找到的话,就返回undefined console.log(b1.a); // undefined // 现在我使用delete运算符删除本地实列属性,那么取到的是就是原型属性了,如下代码: delete b1.name; console.log(b1.name); // AA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function B(){
    this.name = "longen2";
}
B.prototype.name = "AA";
B.prototype.getName = function(){
    return this.name;
};
 
var b1 = new B();
// 在本实列查找,找到就返回,否则到原型查找
console.log(b1.name); // longen2
 
// 在本实列没有找到该方法,就到原型去查找
console.log(b1.getName());//longen2
 
// 如果在本实列没有找到的话,到原型上查找也没有找到的话,就返回undefined
console.log(b1.a); // undefined
 
// 现在我使用delete运算符删除本地实列属性,那么取到的是就是原型属性了,如下代码:
delete b1.name;
console.log(b1.name); // AA

二:理解原型域链的概念

原型的优点是能够以对象结构为载体,创建大量的实列,这些实列能共享原型中的成员(属性和方法);同时也可以使用原型实现面向对象中的继承机制~ 如下代码:下面我们来看这个构造函数AA和构造函数BB,当BB.prototype = new AA(11);执行这个的时候,那么B就继承与A,B中的原型就有x的属性值为11

JavaScript

function AA(x){ this.x = x; } function BB(x) { this.x = x; } BB.prototype = new AA(11); console.log(BB.prototype.x); //11 // 我们再来理解原型继承和原型链的概念,代码如下,都有注释 function A(x) { this.x = x; } // 在A的原型上定义一个属性x = 0 A.prototype.x = 0; function B(x) { this.x = x; } B.prototype = new A(1);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function AA(x){
    this.x = x;
}
function BB(x) {
    this.x = x;
}
BB.prototype = new AA(11);
console.log(BB.prototype.x); //11
 
// 我们再来理解原型继承和原型链的概念,代码如下,都有注释
function A(x) {
    this.x = x;
}
// 在A的原型上定义一个属性x = 0
A.prototype.x = 0;
function B(x) {
    this.x = x;
}
B.prototype = new A(1);

实列化A new A(1)的时候 在A函数内this.x =1, B.prototype = new A(1);B.prototype 是A的实列 也就是B继承于A, 即B.prototype.x = 1;  如下代码:

JavaScript

console.log(B.prototype.x); // 1 // 定义C的构造函数 function C(x) { this.x = x; } C.prototype = new B(2);

1
2
3
4
5
6
console.log(B.prototype.x); // 1
// 定义C的构造函数
function C(x) {
    this.x = x;
}
C.prototype = new B(2);

C.prototype = new B(2); 也就是C.prototype 是B的实列,C继承于B;那么new B(2)的时候 在B的构造函数内 this.x = 2;那么 C的原型上会有一个属性x =2 即C.prototype.x = 2; 如下代码:

JavaScript

console.log(C.prototype.x); // 2

1
console.log(C.prototype.x); // 2

下面是实列化 var d = new C(3); 实列化C的构造函数时候,那么在C的构造函数内this.x = 3; 因此如下打印实列化后的d.x = 3;如下代码:

JavaScript

var d = new C(3); console.log(d.x); // 3

1
2
var d = new C(3);
console.log(d.x); // 3

删除d.x 再访问d.x的时候 本实列对象被删掉,只能从原型上去查找;由于C.prototype = new B(2); 也就是C继承于B,因此C的原型也有x = 2;即C.prototype.x = 2; 如下代码:

JavaScript

delete d.x; console.log(d.x); //2

1
2
delete d.x;
console.log(d.x);  //2

删除C.prototype.x后,我们从上面代码知道,C是继承于B的,自身的原型被删掉后,会去查找父元素的原型链,因此在B的原型上找到x =1; 如下代码:

JavaScript

delete C.prototype.x; console.log(d.x); // 1

1
2
delete C.prototype.x;
console.log(d.x);  // 1

当删除B的原型属性x后,由于B是继承于A的,因此会从父元素的原型链上查找A原型上是否有x的属性,如果有的话,就返回,否则看A是否有继承,没有继承的话,继续往Object上去查找,如果没有找到就返回undefined 因此当删除B的原型x后,delete B.prototype.x; 打印出A上的原型x=0; 如下代码:

JavaScript

delete B.prototype.x; console.log(d.x); // 0 // 继续删除A的原型x后 结果没有找到,就返回undefined了; delete A.prototype.x; console.log(d.x); // undefined

1
2
3
4
5
6
delete B.prototype.x;
console.log(d.x);  // 0
 
// 继续删除A的原型x后 结果没有找到,就返回undefined了;
delete A.prototype.x;
console.log(d.x);  // undefined

在javascript中,一切都是对象,Function和Object都是函数的实列;构造函数的父原型指向于Function原型,Function.prototype的父原型指向与Object的原型,Object的父原型也指向与Function原型,Object.prototype是所有原型的顶层;

如下代码:

JavaScript

Function.prototype.a = function(){ console.log("我是父原型Function"); } Object.prototype.a = function(){ console.log("我是 父原型Object"); } function A(){ this.a = "a"; } A.prototype = { B: function(){ console.log("b"); } } // Function 和 Object都是函数的实列 如下: console.log(A instanceof Function); // true console.log(A instanceof Object); // true // A.prototype是一个对象,它是Object的实列,但不是Function的实列 console.log(A.prototype instanceof Function); // false console.log(A.prototype instanceof Object); // true // Function是Object的实列 同是Object也是Function的实列 console.log(Function instanceof Object); // true console.log(Object instanceof Function); // true /* * Function.prototype是Object的实列 但是Object.prototype不是Function的实列 * 说明Object.prototype是所有父原型的顶层 */ console.log(Function.prototype instanceof Object); //true console.log(Object.prototype instanceof Function); // false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Function.prototype.a = function(){
    console.log("我是父原型Function");
}
Object.prototype.a = function(){
    console.log("我是 父原型Object");
}
function A(){
    this.a = "a";
}
A.prototype = {
    B: function(){
        console.log("b");
    }
}
// Function 和 Object都是函数的实列 如下:
console.log(A instanceof Function);  // true
console.log(A instanceof Object); // true
 
// A.prototype是一个对象,它是Object的实列,但不是Function的实列
console.log(A.prototype instanceof Function); // false
console.log(A.prototype instanceof Object); // true
 
// Function是Object的实列 同是Object也是Function的实列
console.log(Function instanceof Object);   // true
console.log(Object instanceof Function); // true
 
/*
* Function.prototype是Object的实列 但是Object.prototype不是Function的实列
* 说明Object.prototype是所有父原型的顶层
*/
console.log(Function.prototype instanceof Object);  //true
console.log(Object.prototype instanceof Function);  // false

三:理解原型继承机制

构造函数都有一个指针指向原型,Object.prototype是所有原型对象的顶层,比如如下代码:

JavaScript

var obj = {}; Object.prototype.name = "tugenhua"; console.log(obj.name); // tugenhua

1
2
3
var obj = {};
Object.prototype.name = "tugenhua";
console.log(obj.name); // tugenhua

给Object.prototype 定义一个属性,通过字面量构建的对象的话,都会从父类那边获取Object.prototype的属性;

从上面代码我们知道,原型继承的方法是:假如A需要继承于B,那么A.prototype(A的原型) = new B()(作为B的实列) 即可实现A继承于B; 因此我们下面可以初始化一个空的构造函数;然后把对象赋值给构造函数的原型,然后返回该构造函数的实列; 即可实现继承; 如下代码:

JavaScript

if(typeof Object.create !== 'function') { Object.create = function(o) { var F = new Function(); F.prototype = o; return new F(); } } var a = { name: 'longen', getName: function(){ return this.name; } }; var b = {}; b = Object.create(a); console.log(typeof b); //object console.log(b.name); // longen console.log(b.getName()); // longen

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if(typeof Object.create !== 'function') {
    Object.create = function(o) {
        var F = new Function();
        F.prototype = o;
        return new F();
    }
}
var a = {
    name: 'longen',
    getName: function(){
        return this.name;
    }
};
var b = {};
b = Object.create(a);
console.log(typeof b); //object
console.log(b.name);   // longen
console.log(b.getName()); // longen

如上代码:我们先检测Object是否已经有Object.create该方法;如果没有的话就创建一个; 该方法内创建一个空的构造器,把参数对象传递给构造函数的原型,最后返回该构造函数的实列,就实现了继承方式;如上测试代码:先定义一个a对象,有成员属性name=’longen’,还有一个getName()方法;最后返回该name属性; 然后定义一个b空对象,使用Object.create(a);把a对象继承给b对象,因此b对象也有属性name和成员方法getName();

 理解原型查找原理:对象查找先在该构造函数内查找对应的属性,如果该对象没有该属性的话,

那么javascript会试着从该原型上去查找,如果原型对象中也没有该属性的话,那么它们会从原型中的原型去查找,直到查找的Object.prototype也没有该属性的话,那么就会返回undefined;因此我们想要仅在该对象内查找的话,为了提高性能,我们可以使用hasOwnProperty()来判断该对象内有没有该属性,如果有的话,就执行代码(使用for-in循环查找):如下:

JavaScript

var obj = { "name":'tugenhua', "age":'28' }; // 使用for-in循环 for(var i in obj) { if(obj.hasOwnProperty(i)) { console.log(obj[i]); //tugenhua 28 } }

1
2
3
4
5
6
7
8
9
10
var obj = {
    "name":'tugenhua',
    "age":'28'
};
// 使用for-in循环
for(var i in obj) {
    if(obj.hasOwnProperty(i)) {
        console.log(obj[i]); //tugenhua 28
    }
}

如上使用for-in循环查找对象里面的属性,但是我们需要明白的是:for-in循环查找对象的属性,它是不保证顺序的,for-in循环和for循环;最本质的区别是:for循环是有顺序的,for-in循环遍历对象是无序的,因此我们如果需要对象保证顺序的话,可以把对象转换为数组来,然后再使用for循环遍历即可;

下面我们来谈谈原型继承的优点和缺点

JavaScript

// 先看下面的代码: // 定义构造函数A,定义特权属性和特权方法 function A(x) { this.x1 = x; this.getX1 = function(){ return this.x1; } } // 定义构造函数B,定义特权属性和特权方法 function B(x) { this.x2 = x; this.getX2 = function(){ return this.x1 this.x2; } } B.prototype = new A(1);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 先看下面的代码:
// 定义构造函数A,定义特权属性和特权方法
function A(x) {
    this.x1 = x;
    this.getX1 = function(){
        return this.x1;
    }
}
// 定义构造函数B,定义特权属性和特权方法
function B(x) {
    this.x2 = x;
    this.getX2 = function(){
        return this.x1 this.x2;
    }
}
B.prototype = new A(1);

B.prototype = new A(1);这句代码执行的时候,B的原型继承于A,因此B.prototype也有A的属性和方法,即:B.prototype.x1 = 1; B.prototype.getX1 方法;但是B也有自己的特权属性x2和特权方法getX2; 如下代码:

JavaScript

function C(x) { this.x3 = x; this.getX3 = function(){ return this.x3 this.x2; } } C.prototype = new B(2); C.prototype = new B(2);这句代码执行的时候,C的原型继承于B,因此C.prototype.x2 = 2; C.prototype.getX2方法且C也有自己的特权属性x3和特权方法getX3, var b = new B(2); var c = new C(3); console.log(b.x1); // 1 console.log(c.x1); // 1 console.log(c.getX3()); // 5 console.log(c.getX2()); // 3 var b = new B(2);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function C(x) {
    this.x3 = x;
    this.getX3 = function(){
        return this.x3 this.x2;
    }
}
C.prototype = new B(2);
C.prototype = new B(2);这句代码执行的时候,C的原型继承于B,因此C.prototype.x2 = 2; C.prototype.getX2方法且C也有自己的特权属性x3和特权方法getX3,
var b = new B(2);
var c = new C(3);
console.log(b.x1);  // 1
console.log(c.x1);  // 1
console.log(c.getX3()); // 5
console.log(c.getX2()); // 3
var b = new B(2);

实列化B的时候 b.x1 首先会在构造函数内查找x1属性,没有找到,由于B的原型继承于A,因此A有x1属性,因此B.prototype.x1 = 1找到了;var c = new C(3); 实列化C的时候,从上面的代码可以看到C继承于B,B继承于A,因此在C函数中没有找到x1属性,会往原型继续查找,直到找到父元素A有x1属性,因此c.x1 = 1;c.getX3()方法; 返回this.x3 this.x2 this.x3 = 3;this.x2 是B的属性,因此this.x2 = 2;c.getX2(); 查找的方法也一样,不再解释

prototype的缺点与优点如下:

优点是:能够允许多个对象实列共享原型对象的成员及方法,

缺点是:1. 每个构造函数只有一个原型,因此不直接支持多重继承;

2. 不能很好地支持多参数或动态参数的父类。在原型继承阶段,用户还不能决定以

什么参数来实列化构造函数。

四:理解使用类继承(继承的更好的方案)

类继承也叫做构造函数继承,在子类中执行父类的构造函数;实现原理是:可以将一个构造函数A的方法赋值给另一个构造函数B,然后调用该方法,使构造函数A在构造函数B内部被执行,这时候构造函数B就拥有了构造函数A中的属性和方法,这就是使用类继承实现B继承与A的基本原理;

如下代码实现demo:

JavaScript

function A(x) { this.x = x; this.say = function(){ return this.x; } } function B(x,y) { this.m = A; // 把构造函数A作为一个普通函数引用给临时方法m this.m(x); // 执行构造函数A; delete this.m; // 清除临时方法this.m this.y = y; this.method = function(){ return this.y; } } var a = new A(1); var b = new B(2,3); console.log(a.say()); //输出1, 执行构造函数A中的say方法 console.log(b.say()); //输出2, 能执行该方法说明被继承了A中的方法 console.log(b.method()); // 输出3, 构造函数也拥有自己的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function A(x) {
    this.x = x;
    this.say = function(){
        return this.x;
    }
}
function B(x,y) {
    this.m = A; // 把构造函数A作为一个普通函数引用给临时方法m
    this.m(x);  // 执行构造函数A;
    delete this.m; // 清除临时方法this.m
    this.y = y;
    this.method = function(){
        return this.y;
    }
}
var a = new A(1);
var b = new B(2,3);
console.log(a.say()); //输出1, 执行构造函数A中的say方法
console.log(b.say()); //输出2, 能执行该方法说明被继承了A中的方法
console.log(b.method()); // 输出3, 构造函数也拥有自己的方法

上面的代码实现了简单的类继承的基础,但是在复杂的编程中是不会使用上面的方法的,因为上面的代码不够严谨;代码的耦合性高;我们可以使用更好的方法如下:

JavaScript

function A(x) { this.x = x; } A.prototype.getX = function(){ return this.x; } // 实例化A var a = new A(1); console.log(a.x); // 1 console.log(a.getX()); // 输出1 // 现在我们来创建构造函数B,让其B继承与A,如下代码: function B(x,y) { this.y = y; A.call(this,x); } B.prototype = new A(); // 原型继承 console.log(B.prototype.constructor); // 输出构造函数A,指针指向与构造函数A B.prototype.constructor = B; // 重新设置构造函数,使之指向B console.log(B.prototype.constructor); // 指向构造函数B B.prototype.getY = function(){ return this.y; } var b = new B(1,2); console.log(b.x); // 1 console.log(b.getX()); // 1 console.log(b.getY()); // 2 // 下面是演示对构造函数getX进行重写的方法如下: B.prototype.getX = function(){ return this.x; } var b2 = new B(10,20); console.log(b2.getX()); // 输出10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function A(x) {
    this.x = x;
}
A.prototype.getX = function(){
    return this.x;
}
// 实例化A
var a = new A(1);
console.log(a.x); // 1
console.log(a.getX()); // 输出1
// 现在我们来创建构造函数B,让其B继承与A,如下代码:
function B(x,y) {
    this.y = y;
    A.call(this,x);
}
B.prototype = new A();  // 原型继承
console.log(B.prototype.constructor); // 输出构造函数A,指针指向与构造函数A
B.prototype.constructor = B;          // 重新设置构造函数,使之指向B
console.log(B.prototype.constructor); // 指向构造函数B
B.prototype.getY = function(){
    return this.y;
}
var b = new B(1,2);
console.log(b.x); // 1
console.log(b.getX()); // 1
console.log(b.getY()); // 2
 
// 下面是演示对构造函数getX进行重写的方法如下:
B.prototype.getX = function(){
    return this.x;
}
var b2 = new B(10,20);
console.log(b2.getX());  // 输出10

下面我们来分析上面的代码:

在构造函数B内,使用A.call(this,x);这句代码的含义是:我们都知道使用call或者apply方法可以改变this指针指向,从而可以实现类的继承,因此在B构造函数内,把x的参数传递给A构造函数,并且继承于构造函数A中的属性和方法;

使用这句代码:B.prototype = new A();  可以实现原型继承,也就是B可以继承A中的原型所有的方法;console.log(B.prototype.constructor); 打印出输出构造函数A,指针指向与构造函数A;我们明白的是,当定义构造函数时候,其原型对象默认是一个Object类型的一个实例,其构造器默认会被设置为构造函数本身,如果改动构造函数prototype属性值,使其指向于另一个对象的话,那么新对象就不会拥有原来的constructor的值,比如第一次打印console.log(B.prototype.constructor); 指向于被实例化后的构造函数A,重写设置B的constructor的属性值的时候,第二次打印就指向于本身B;因此B继承与构造A及其原型的所有属性和方法,当然我们也可以对构造函数B重写构造函数A中的方法,如上面最后几句代码是对构造函数A中的getX方法进行重写,来实现自己的业务~;

五:建议使用封装类实现继承

封装类实现继承的基本原理:先定义一个封装函数extend;该函数有2个参数,Sub代表子类,Sup代表超类;在函数内,先定义一个空函数F, 用来实现功能中转,先设置F的原型为超类的原型,然后把空函数的实例传递给子类的原型,使用一个空函数的好处是:避免直接实例化超类可能会带来系统性能问题,比如超类的实例很大的话,实例化会占用很多内存;

如下代码:

JavaScript

function extend(Sub,Sup) { //Sub表示子类,Sup表示超类 // 首先定义一个空函数 var F = function(){}; // 设置空函数的原型为超类的原型 F.prototype = Sup.prototype; // 实例化空函数,并把超类原型引用传递给子类 Sub.prototype = new F(); // 重置子类原型的构造器为子类自身 Sub.prototype.constructor = Sub; // 在子类中保存超类的原型,避免子类与超类耦合 Sub.sup = Sup.prototype; if(Sup.prototype.constructor === Object.prototype.constructor) { // 检测超类原型的构造器是否为原型自身 Sup.prototype.constructor = Sup; } } 测试代码如下: // 下面我们定义2个类A和类B,我们目的是实现B继承于A function A(x) { this.x = x; this.getX = function(){ return this.x; } } A.prototype.add = function(){ return this.x this.x; } A.prototype.mul = function(){ return this.x * this.x; } // 构造函数B function B(x){ A.call(this,x); // 继承构造函数A中的所有属性及方法 } extend(B,A); // B继承于A var b = new B(11); console.log(b.getX()); // 11 console.log(b.add()); // 22 console.log(b.mul()); // 121

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function extend(Sub,Sup) {
    //Sub表示子类,Sup表示超类
    // 首先定义一个空函数
    var F = function(){};
 
    // 设置空函数的原型为超类的原型
    F.prototype = Sup.prototype;
 
// 实例化空函数,并把超类原型引用传递给子类
    Sub.prototype = new F();
 
    // 重置子类原型的构造器为子类自身
    Sub.prototype.constructor = Sub;
 
    // 在子类中保存超类的原型,避免子类与超类耦合
    Sub.sup = Sup.prototype;
 
    if(Sup.prototype.constructor === Object.prototype.constructor) {
        // 检测超类原型的构造器是否为原型自身
        Sup.prototype.constructor = Sup;
    }
 
}
测试代码如下:
// 下面我们定义2个类A和类B,我们目的是实现B继承于A
function A(x) {
    this.x = x;
    this.getX = function(){
        return this.x;
    }
}
A.prototype.add = function(){
    return this.x this.x;
}
A.prototype.mul = function(){
    return this.x * this.x;
}
// 构造函数B
function B(x){
    A.call(this,x); // 继承构造函数A中的所有属性及方法
}
extend(B,A);  // B继承于A
var b = new B(11);
console.log(b.getX()); // 11
console.log(b.add());  // 22
console.log(b.mul());  // 121

注意:在封装函数中,有这么一句代码:Sub.sup = Sup.prototype; 我们现在可以来理解下它的含义:

比如在B继承与A后,我给B函数的原型再定义一个与A相同的原型相同的方法add();

如下代码

JavaScript

extend(B,A); // B继承于A var b = new B(11); B.prototype.add = function(){ return this.x "" this.x; } console.log(b.add()); // 1111

1
2
3
4
5
6
extend(B,A);  // B继承于A
var b = new B(11);
B.prototype.add = function(){
    return this.x "" this.x;
}
console.log(b.add()); // 1111

那么B函数中的add方法会覆盖A函数中的add方法;因此为了不覆盖A类中的add()方法,且调用A函数中的add方法;可以如下编写代码:

JavaScript

B.prototype.add = function(){ //return this.x "" this.x; return B.sup.add.call(this); } console.log(b.add()); // 22

1
2
3
4
5
B.prototype.add = function(){
    //return this.x "" this.x;
    return B.sup.add.call(this);
}
console.log(b.add()); // 22

B.sup.add.call(this); 中的B.sup就包含了构造函数A函数的指针,因此包含A函数的所有属性和方法;因此可以调用A函数中的add方法;

如上是实现继承的几种方式,类继承和原型继承,但是这些继承无法继承DOM对象,也不支持继承系统静态对象,静态方法等;比如Date对象如下:

JavaScript

// 使用类继承Date对象 function D(){ Date.apply(this,arguments); // 调用Date对象,对其引用,实现继承 } var d = new D(); console.log(d.toLocaleString()); // [object object]

1
2
3
4
5
6
// 使用类继承Date对象
function D(){
    Date.apply(this,arguments); // 调用Date对象,对其引用,实现继承
}
var d = new D();
console.log(d.toLocaleString()); // [object object]

如上代码运行打印出object,我们可以看到使用类继承无法实现系统静态方法date对象的继承,因为他不是简单的函数结构,对声明,赋值和初始化都进行了封装,因此无法继承;

下面我们再来看看使用原型继承date对象;

JavaScript

function D(){} D.prototype = new D(); var d = new D(); console.log(d.toLocaleString());//[object object]

1
2
3
4
function D(){}
D.prototype = new D();
var d = new D();
console.log(d.toLocaleString());//[object object]

我们从代码中看到,使用原型继承也无法继承Date静态方法;但是我们可以如下封装代码继承:

JavaScript

function D(){ var d = new Date(); // 实例化Date对象 d.get = function(){ // 定义本地方法,间接调用Date对象的方法 console.log(d.toLocaleString()); } return d; } var d = new D(); d.get(); // 2015/12/21 上午12:08:38

1
2
3
4
5
6
7
8
9
function D(){
    var d = new Date();  // 实例化Date对象
    d.get = function(){ // 定义本地方法,间接调用Date对象的方法
        console.log(d.toLocaleString());
    }
    return d;
}
var d = new D();
d.get(); // 2015/12/21 上午12:08:38

六:理解使用复制继承

复制继承的基本原理是:先设计一个空对象,然后使用for-in循环来遍历对象的成员,将该对象的成员一个一个复制给新的空对象里面;这样就实现了复制继承了;如下代码:

JavaScript

function A(x,y) { this.x = x; this.y = y; this.add = function(){ return this.x this.y; } } A.prototype.mul = function(){ return this.x * this.y; } var a = new A(2,3); var obj = {}; for(var i in a) { obj[i] = a[i]; } console.log(obj); // object console.log(obj.x); // 2 console.log(obj.y); // 3 console.log(obj.add()); // 5 console.log(obj.mul()); // 6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function A(x,y) {
    this.x = x;
    this.y = y;
    this.add = function(){
        return this.x this.y;
    }
}
A.prototype.mul = function(){
    return this.x * this.y;
}
var a = new A(2,3);
var obj = {};
for(var i in a) {
    obj[i] = a[i];
}
console.log(obj); // object
console.log(obj.x); // 2
console.log(obj.y); // 3
console.log(obj.add()); // 5
console.log(obj.mul()); // 6

如上代码:先定义一个构造函数A,函数里面有2个属性x,y,还有一个add方法,该构造函数原型有一个mul方法,首先实列化下A后,再创建一个空对象obj,遍历对象一个个复制给空对象obj,从上面的打印效果来看,我们可以看到已经实现了复制继承了;对于复制继承,我们可以封装成如下方法来调用:

JavaScript

// 为Function扩展复制继承方法 Function.prototype.extend = function(o) { for(var i in o) { //把参数对象的成员复制给当前对象的构造函数原型对象 this.constructor.prototype[i] = o[i]; } } // 测试代码如下: var o = function(){}; o.extend(new A(1,2)); console.log(o.x); // 1 console.log(o.y); // 2 console.log(o.add()); // 3 console.log(o.mul()); // 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 为Function扩展复制继承方法
Function.prototype.extend = function(o) {
    for(var i in o) {
        //把参数对象的成员复制给当前对象的构造函数原型对象
        this.constructor.prototype[i] = o[i];
    }
}
// 测试代码如下:
var o = function(){};
o.extend(new A(1,2));
console.log(o.x);  // 1
console.log(o.y);  // 2
console.log(o.add()); // 3
console.log(o.mul()); // 2

上面封装的扩展继承方法中的this对象指向于当前实列化后的对象,而不是指向于构造函数本身,因此要使用原型扩展成员的话,就需要使用constructor属性来指向它的构造器,然后通过prototype属性指向构造函数的原型;

复制继承有如下优点:

1. 它不能继承系统核心对象的只读方法和属性

2. 如果对象数据非常多的话,这样一个个复制的话,性能是非常低的;

3. 只有对象被实列化后,才能给遍历对象的成员和属性,相对来说不够灵活;

4. 复制继承只是简单的赋值,所以如果赋值的对象是引用类型的对象的话,可能会存在一些副作用;如上我们看到有如上一些缺点,下面我们可以使用clone(克隆的方式)来优化下:

基本思路是:为Function扩展一个方法,该方法能够把参数对象赋值赋值一个空构造函数的原型对象,然后实列化构造函数并返回实列对象,这样该对象就拥有了该对象的所有成员;代码如下:

JavaScript

Function.prototype.clone = function(o){ function Temp(){}; Temp.prototype = o; return Temp(); } // 测试代码如下: Function.clone(new A(1,2)); console.log(o.x); // 1 console.log(o.y); // 2 console.log(o.add()); // 3 console.log(o.mul()); // 2

1
2
3
4
5
6
7
8
9
10
11
Function.prototype.clone = function(o){
    function Temp(){};
    Temp.prototype = o;
    return Temp();
}
// 测试代码如下:
Function.clone(new A(1,2));
console.log(o.x);  // 1
console.log(o.y);  // 2
console.log(o.add()); // 3
console.log(o.mul()); // 2

2 赞 19 收藏 1 评论

图片 1

一:理解构造函数原型(prototype)机制

深入理解JavaScript系列(18):面向对象编程之ECMAScript实现,

介绍

本章是关于ECMAScript面向对象实现的第2篇,第1篇我们讨论的是概论和CEMAScript的比较,如果你还没有读第1篇,在进行本章之前,我强烈建议你先读一下第1篇,因为本篇实在太长了(35页)。

英文原文:
注:由于篇幅太长了,难免出现错误,时刻保持修正中。

在概论里,我们延伸到了ECMAScript,现在,当我们知道它OOP实现时,我们再来准确定义一下:
复制代码 代码如下:
ECMAScript is an object-oriented programming language supporting delegating inheritance based on prototypes.

ECMAScript是一种面向对象语言,支持基于原型的委托式继承。
我们将从最基本的数据类型来分析,首先要了解的是ECMAScript用原始值(primitive values)和对象(objects)来区分实体,因此有些文章里说的“在JavaScript里,一切都是对象”是错误的(不完全对),原始值就是我们这里要讨论的一些数据类型。

数据类型

虽然ECMAScript是可以动态转化类型的动态弱类型语言,它还是有数据类型的。也就是说,一个对象要属于一个实实在在的类型。
标准规范里定义了9种数据类型,但只有6种是在ECMAScript程序里可以直接访问的,它们是:Undefined、Null、Boolean、String、Number、Object。

另外3种类型只能在实现级别访问(ECMAScript对象是不能使用这些类型的)并用于规范来解释一些操作行为、保存中间值。这3种类型是:Reference、List和Completion。

因此,Reference是用来解释delete、typeof、this这样的操作符,并且包含一个基对象和一个属性名称;List描述的是参数列表的行为(在new表达式和函数调用的时候);Completion是用来解释行为break、continue、return和throw语句的。

原始值类型 回头来看6中用于ECMAScript程序的数据类型,前5种是原始值类型,包括Undefined、Null、Boolean、String、Number、Object。
原始值类型例子:
复制代码 代码如下:
var a = undefined;
var b = null;
var c = true;
var d = 'test';
var e = 10;

这些值是在底层上直接实现的,他们不是object,所以没有原型,没有构造函数。

大叔注:这些原生值和我们平时用的(Boolean、String、Number、Object)虽然名字上相似,但不是同一个东西。所以typeof(true)和typeof(Boolean)结果是不一样的,因为typeof(Boolean)的结果是function,所以函数Boolean、String、Number是有原型的(下面的读写属性章节也会提到)。

想知道数据是哪种类型用typeof是最好不过了,有个例子需要注意一下,如果用typeof来判断null的类型,结果是object,为什么呢?因为null的类型是定义为Null的。
复制代码 代码如下:
alert(typeof null); // "object"

显示"object"原因是因为规范就是这么规定的:对于Null值的typeof字符串值返回"object“。

规范没有想象解释这个,但是Brendan Eich (JavaScript发明人)注意到null相对于undefined大多数都是用于对象出现的地方,例如设置一个对象为空引用。但是有些文档里有些气人将之归结为bug,而且将该bug放在Brendan Eich也参与讨论的bug列表里,结果就是任其自然,还是把typeof null的结果设置为object(尽管262-3的标准是定义null的类型是Null,262-5已经将标准修改为null的类型是object了)。

Object类型

接着,Object类型(不要和Object构造函数混淆了,现在只讨论抽象类型)是描述 ECMAScript对象的唯一一个数据类型。

Object is an unordered collection of key-value pairs.
对象是一个包含key-value对的无序集合

对象的key值被称为属性,属性是原始值和其他对象的容器。如果属性的值是函数我们称它为方法 。

例如:
复制代码 代码如下:
var x = { // 对象"x"有3个属性: a, b, c
  a: 10, // 原始值
  b: {z: 100}, // 对象"b"有一个属性z
  c: function () { // 函数(方法)
    alert('method x.c');
  }
};
 
alert(x.a); // 10
alert(x.b); // [object Object]
alert(x.b.z); // 100
x.c(); // 'method x.c'

动态性

正如我们在第17章中指出的,ES中的对象是完全动态的。这意味着,在程序执行的时候我们可以任意地添加,修改或删除对象的属性。

例如:
复制代码 代码如下:
var foo = {x: 10};
 
// 添加新属性
foo.y = 20;
console.log(foo); // {x: 10, y: 20}
 
// 将属性值修改为函数
foo.x = function () {
  console.log('foo.x');
};
 
foo.x(); // 'foo.x'
 
// 删除属性
delete foo.x;
console.log(foo); // {y: 20}

有些属性不能被修改——(只读属性、已删除属性或不可配置的属性)。 我们将稍后在属性特性里讲解。

另外,ES5规范规定,静态对象不能扩展新的属性,并且它的属性页不能删除或者修改。他们是所谓的冻结对象,可以通过应用Object.freeze(o)方法得到。
复制代码 代码如下:
var foo = {x: 10};
 
// 冻结对象
Object.freeze(foo);
console.log(Object.isFrozen(foo)); // true
 
// 不能修改
foo.x = 100;
 
// 不能扩展
foo.y = 200;
 
// 不能删除
delete foo.x;
 
console.log(foo); // {x: 10}

在ES5规范里,也使用Object.preventExtensions(o)方法防止扩展,或者使用Object.defineProperty(o)方法来定义属性:
复制代码 代码如下:
var foo = {x : 10};
 
Object.defineProperty(foo, "y", {
  value: 20,
  writable: false, // 只读
  configurable: false // 不可配置
});
 
// 不能修改
foo.y = 200;
 
// 不能删除
delete foo.y; // false
 
// 防治扩展
Object.preventExtensions(foo);
console.log(Object.isExtensible(foo)); // false
 
// 不能添加新属性
foo.z = 30;
 
console.log(foo); {x: 10, y: 20}

内置对象、原生对象及宿主对象

有必要需要注意的是规范还区分了这内置对象、元素对象和宿主对象。

内置对象和元素对象是被ECMAScript规范定义和实现的,两者之间的差异微不足道。所有ECMAScript实现的对象都是原生对象(其中一些是内置对象、一些在程序执行的时候创建,例如用户自定义对象)。内置对象是原生对象的一个子集、是在程序开始之前内置到ECMAScript里的(例如,parseInt, Match等)。所有的宿主对象是由宿主环境提供的,通常是浏览器,并可能包括如window、alert等。

注意,宿主对象可能是ES自身实现的,完全符合规范的语义。从这点来说,他们能称为“原生宿主”对象(尽快很理论),不过规范没有定义“原生宿主”对象的概念。

Boolean,String和Number对象

另外,规范也定义了一些原生的特殊包装类,这些对象是:

1.布尔对象
2.字符串对象
3.数字对象

这些对象的创建,是通过相应的内置构造器创建,并且包含原生值作为其内部属性,这些对象可以转换省原始值,反之亦然。

复制代码 代码如下:
var c = new Boolean(true);
var d = new String('test');
var e = new Number(10);
 
// 转换成原始值
// 使用不带new关键字的函数
с = Boolean(c);
d = String(d);
e = Number(e);
 
// 重新转换成对象
с = Object(c);
d = Object(d);
e = Object(e);

此外,也有对象是由特殊的内置构造函数创建: Function(函数对象构造器)、Array(数组构造器) RegExp(正则表达式构造器)、Math(数学模块)、 Date(日期的构造器)等等,这些对象也是Object对象类型的值,他们彼此的区别是由内部属性管理的,我们在下面讨论这些内容。

字面量Literal

对于三个对象的值:对象(object),数组(array)和正则表达式(regular expression),他们分别有简写的标示符称为:对象初始化器、数组初始化器、和正则表达式初始化器:
复制代码 代码如下:
// 等价于new Array(1, 2, 3);
// 或者array = new Array();
// array[0] = 1;
// array[1] = 2;
// array[2] = 3;
var array = [1, 2, 3];
 
// 等价于
// var object = new Object();
// object.a = 1;
// object.b = 2;
// object.c = 3;
var object = {a: 1, b: 2, c: 3};
 
// 等价于new RegExp("^\d $", "g")
var re = /^d $/g;

注意,如果上述三个对象进行重新赋值名称到新的类型上的话,那随后的实现语义就是按照新赋值的类型来使用,例如在当前的Rhino和老版本SpiderMonkey 1.7的实现上,会成功以new关键字的构造器来创建对象,但有些实现(当前Spider/TraceMonkey)字面量的语义在类型改变以后却不一定改变。
复制代码 代码如下:
var getClass = Object.prototype.toString;
 
Object = Number;
 
var foo = new Object;
alert([foo, getClass.call(foo)]); // 0, "[object Number]"
 
var bar = {};
 
// Rhino, SpiderMonkey 1.7中 - 0, "[object Number]"
// 其它: still "[object Object]", "[object Object]"
alert([bar, getClass.call(bar)]);
 
// Array也是一样的效果
Array = Number;
 
foo = new Array;
alert([foo, getClass.call(foo)]); // 0, "[object Number]"
 
bar = [];
 
// Rhino, SpiderMonkey 1.7中 - 0, "[object Number]"
// 其它: still "", "[object Object]"
alert([bar, getClass.call(bar)]);
 
// 但对RegExp,字面量的语义是不被改变的。 semantics of the literal
// isn't being changed in all tested implementations
 
RegExp = Number;
 
foo = new RegExp;
alert([foo, getClass.call(foo)]); // 0, "[object Number]"
 
bar = /(?!)/g;
alert([bar, getClass.call(bar)]); // /(?!)/g, "[object RegExp]"

正则表达式字面量和RegExp对象

注意,下面2个例子在第三版的规范里,正则表达式的语义都是等价的,regexp字面量只在一句里存在,并且再解析阶段创建,但RegExp构造器创建的却是新对象,所以这可能会导致出一些问题,如lastIndex的值在测试的时候结果是错误的:
复制代码 代码如下:
for (var k = 0; k < 4; k ) {
  var re = /ecma/g;
  alert(re.lastIndex); // 0, 4, 0, 4
  alert(re.test("ecmascript")); // true, false, true, false
}
 
// 对比
 
for (var k = 0; k < 4; k ) {
  var re = new RegExp("ecma", "g");
  alert(re.lastIndex); // 0, 0, 0, 0
  alert(re.test("ecmascript")); // true, true, true, true
}

注:不过这些问题在第5版的ES规范都已经修正了,不管是基于字面量的还是构造器的,正则都是创建新对象。

关联数组

各种文字静态讨论,JavaScript对象(经常是用对象初始化器{}来创建)被称为哈希表哈希表或其它简单的称谓:哈希(Ruby或Perl里的概念), 管理数组(PHP里的概念),词典 (Python里的概念)等。

只有这样的术语,主要是因为他们的结构都是相似的,就是使用“键-值”对来存储对象,完全符合“关联数组 ”或“哈希表 ”理论定义的数据结构。 此外,哈希表抽象数据类型通常是在实现层面使用。

但是,尽管术语上来描述这个概念,但实际上这个是错误,从ECMAScript来看:ECMAScript只有一个对象以及类型以及它的子类型,这和“键-值”对存储没有什么区别,因此在这上面没有特别的概念。 因为任何对象的内部属性都可以存储为键-值”对:
复制代码 代码如下:
var a = {x: 10};
a['y'] = 20;
a.z = 30;
 
var b = new Number(1);
b.x = 10;
b.y = 20;
b['z'] = 30;
 
var c = new Function('');
c.x = 10;
c.y = 20;
c['z'] = 30;
 
// 等等,任意对象的子类型"subtype"

此外,由于在ECMAScript中对象可以是空的,所以"hash"的概念在这里也是不正确的:
复制代码 代码如下:
Object.prototype.x = 10;
 
var a = {}; // 创建空"hash"
 
alert(a["x"]); // 10, 但不为空
alert(a.toString); // function
 
a["y"] = 20; // 添加新的键值对到 "hash"
alert(a["y"]); // 20
 
Object.prototype.y = 20; // 添加原型属性
 
delete a["y"]; // 删除
alert(a["y"]); // 但这里key和value依然有值 – 20

请注意, ES5标准可以让我们创建没原型的对象(使用Object.create(null)方法实现)对,从这个角度来说,这样的对象可以称之为哈希表:
复制代码 代码如下:
var aHashTable = Object.create(null);
console.log(aHashTable.toString); // 未定义

此外,一些属性有特定的getter / setter方法​​,所以也可能导致混淆这个概念:
复制代码 代码如下:
var a = new String("foo");
a['length'] = 10;
alert(a['length']); // 3

然而,即使认为“哈希”可能有一个“原型”(例如,在Ruby或Python里委托哈希对象的类),在ECMAScript里,这个术语也是不对的,因为2个表示法之间没有语义上的区别(即用点表示法a.b和a["b"]表示法)。

在ECMAScript中的“property属性”的概念语义上和"key"、数组索引、方法没有分开的,这里所有对象的属性读写都要遵循统一的规则:检查原型链。

在下面Ruby的例子中,我们可以看到语义上的区别:
复制代码 代码如下:
a = {}
a.class # Hash
 
a.length # 0
 
# new "key-value" pair
a['length'] = 10;
 
# 语义上,用点访问的是属性或方法,而不是key
 
a.length # 1
 
# 而索引器访问访问的是hash里的key
 
a['length'] # 10
 
# 就类似于在现有对象上动态声明Hash类
# 然后声明新属性或方法
 
class Hash
  def z
    100
  end
end
 
# 新属性可以访问
 
a.z # 100
 
# 但不是"key"
 
a['z'] # nil

ECMA-262-3标准并没有定义“哈希”(以及类似)的概念。但是,有这样的结构理论的话,那可能以此命名的对象。

对象转换

将对象转化成原始值可以用valueOf方法,正如我们所说的,当函数的构造函数调用做为function(对于某些类型的),但如果不用new关键字就是将对象转化成原始值,就相当于隐式的valueOf方法调用:
复制代码 代码如下:
var a = new Number(1);
var primitiveA = Number(a); // 隐式"valueOf"调用
var alsoPrimitiveA = a.valueOf(); // 显式调用
 
alert([
  typeof a, // "object"
  typeof primitiveA, // "number"
  typeof alsoPrimitiveA // "number"
]);

这种方式允许对象参与各种操作,例如:
复制代码 代码如下:
var a = new Number(1);
var b = new Number(2);
 
alert(a b); // 3
 
// 甚至
 
var c = {
  x: 10,
  y: 20,
  valueOf: function () {
    return this.x this.y;
  }
};
 
var d = {
  x: 30,
  y: 40,
  // 和c的valueOf功能一样
  valueOf: c.valueOf
};
 
alert(c d); // 100

valueOf的默认值会根据根据对象的类型改变(如果不被覆盖的话),对某些对象,他返回的是this——例如:Object.prototype.valueOf(),还有计算型的值:Date.prototype.valueOf()返回的是日期时间:
复制代码 代码如下:
var a = {};
alert(a.valueOf() === a); // true, "valueOf"返回this
 
var d = new Date();
alert(d.valueOf()); // time
alert(d.valueOf() === d.getTime()); // true

此外,对象还有一个更原始的代表性——字符串展示。 这个toString方法是可靠的,它在某些操作上是自动使用的:
复制代码 代码如下:
var a = {
  valueOf: function () {
    return 100;
  },
  toString: function () {
    return '__test';
  }
};
 
// 这个操作里,toString方法自动调用
alert(a); // "__test"
 
// 但是这里,调用的却是valueOf()方法
alert(a 10); // 110
 
// 但,一旦valueOf删除以后
// toString又可以自动调用了
delete a.valueOf;
alert(a 10); // "_test10"

Object.prototype上定义的toString方法具有特殊意义,它返回的我们下面将要讨论的内部[[Class]]属性值。

和转化成原始值(ToPrimitive)相比,将值转化成对象类型也有一个转化规范(ToObject)。

一个显式方法是使用内置的Object构造函数作为function来调用ToObject(有些类似通过new关键字也可以):
复制代码 代码如下:
var n = Object(1); // [object Number]
var s = Object('test'); // [object String]
 
// 一些类似,使用new操作符也可以
var b = new Object(true); // [object Boolean]
 
// 应用参数new Object的话创建的是简单对象
var o = new Object(); // [object Object]
 
// 如果参数是一个现有的对象
// 那创建的结果就是简单返回该对象
var a = [];
alert(a === new Object(a)); // true
alert(a === Object(a)); // true

关于调用内置构造函数,使用还是不适用new操作符没有通用规则,取决于构造函数。 例如Array或Function当使用new操作符的构造函数或者不使用new操作符的简单函数使用产生相同的结果的:
复制代码 代码如下:
var a = Array(1, 2, 3); // [object Array]
var b = new Array(1, 2, 3); // [object Array]
var c = [1, 2, 3]; // [object Array]
 
var d = Function(''); // [object Function]
var e = new Function(''); // [object Function]

有些操作符使用的时候,也有一些显示和隐式转化:
复制代码 代码如下:
var a = 1;
var b = 2;
 
// 隐式
var c = a b; // 3, number
var d = a b '5' // "35", string
 
// 显式
var e = '10'; // "10", string
var f = e; // 10, number
var g = parseInt(e, 10); // 10, number
 
// 等等

属性的特性

所有的属性(property) 都可以有很多特性(attributes)。

1.{ReadOnly}——忽略向属性赋值的写操作尝,但只读属性可以由宿主环境行为改变——也就是说不是“恒定值” ;
2.{DontEnum}——属性不能被for..in循环枚举
3.{DontDelete}——糊了delete操作符的行为被忽略(即删不掉);
4.{Internal}——内部属性,没有名字(仅在实现层面使用),ECMAScript里无法访问这样的属性。

注意,在ES5里{ReadOnly},{DontEnum}和{DontDelete}被重新命名为[[Writable]],[[Enumerable]]和[[Configurable]],可以手工通过Object.defineProperty或类似的方法来管理这些属性。

复制代码 代码如下:
var foo = {};
 
Object.defineProperty(foo, "x", {
  value: 10,
  writable: true, // 即{ReadOnly} = false
  enumerable: false, // 即{DontEnum} = true
  configurable: true // 即{DontDelete} = false
});
 
console.log(foo.x); // 10
 
// 通过descriptor获取特性集attributes
var desc = Object.getOwnPropertyDescriptor(foo, "x");
 
console.log(desc.enumerable); // false
console.log(desc.writable); // true
// 等等

内部属性和方法

对象也可以有内部属性(实现层面的一部分),并且ECMAScript程序无法直接访问(但是下面我们将看到,一些实现允许访问一些这样的属性)。 这些属性通过嵌套的中括号[[ ]]进行访问。我们来看其中的一些,这些属性的描述可以到规范里查阅到。

每个对象都应该实现如下内部属性和方法:

1.[[Prototype]]——对象的原型(将在下面详细介绍)
2.[[Class]]——字符串对象的一种表示(例如,Object Array ,Function Object,Function等);用来区分对象
3.[[Get]]——获得属性值的方法
4.[[Put]]——设置属性值的方法
5.[[CanPut]]——检查属性是否可写
6.[[HasProperty]]——检查对象是否已经拥有该属性
7.[[Delete]]——从对象删除该属性
8.[[DefaultValue]]返回对象对于的原始值(调用valueOf方法,某些对象可能会抛出TypeError异常)。
通过Object.prototype.toString()方法可以间接得到内部属性[[Class]]的值,该方法应该返回下列字符串: "[object " [[Class]] "]" 。例如:
复制代码 代码如下:
var getClass = Object.prototype.toString;
 
getClass.call({}); // [object Object]
getClass.call([]); // [object Array]
getClass.call(new Number(1)); // [object Number]
// 等等

这个功能通常是用来检查对象用的,但规范上说宿主对象的[[Class]]可以为任意值,包括内置对象的[[Class]]属性的值,所以理论上来看是不能100%来保证准确的。例如,document.childNodes.item(...)方法的[[Class]]属性,在IE里返回"String",但其它实现里返回的确实"Function"。
复制代码 代码如下:
// in IE - "String", in other - "Function"
alert(getClass.call(document.childNodes.item));

构造函数

因此,正如我们上面提到的,在ECMAScript中的对象是通过所谓的构造函数来创建的。

Constructor is a function that creates and initializes the newly created object.
构造函数是一个函数,用来创建并初始化新创建的对象。
对象创建(内存分配)是由构造函数的内部方法[[Construct]]负责的。该内部方法的行为是定义好的,所有的构造函数都是使用该方法来为新对象分配内存的。

而初始化是通过新建对象上下上调用该函数来管理的,这是由构造函数的内部方法[[Call]]来负责任的。

注意,用户代码只能在初始化阶段访问,虽然在初始化阶段我们可以返回不同的对象(忽略第一阶段创建的tihs对象):
复制代码 代码如下:
function A() {
  // 更新新创建的对象
  this.x = 10;
  // 但返回的是不同的对象
  return [1, 2, 3];
}
 
var a = new A();
console.log(a.x, a); undefined, [1, 2, 3]

引用15章函数——创建函数的算法小节,我们可以看到该函数是一个原生对象,包含[[Construct]] ]和[[Call]] ]属性以及显示的prototype原型属性——未来对象的原型(注:NativeObject是对于native object原生对象的约定,在下面的伪代码中使用)。
复制代码 代码如下:
F = new NativeObject();
 
F.[[Class]] = "Function"
 
.... // 其它属性
 
F.[[Call]] = <reference to function> // function自身
 
F.[[Construct]] = internalConstructor // 普通的内部构造函数
 
.... // 其它属性
 
// F构造函数创建的对象原型
__objectPrototype = {};
__objectPrototype.constructor = F // {DontEnum}
F.prototype = __objectPrototype

[[Call]] ]是除[[Class]]属性(这里等同于"Function" )之外区分对象的主要方式,因此,对象的内部[[Call]]属性作为函数调用。 这样的对象用typeof运算操作符的话返回的是"function"。然而它主要是和原生对象有关,有些情况的实现在用typeof获取值的是不一样的,例如:window.alert (...)在IE中的效果:
复制代码 代码如下:
// IE浏览器中 - "Object", "object", 其它浏览器 - "Function", "function"
alert(Object.prototype.toString.call(window.alert));
alert(typeof window.alert); // "Object"

内部方法[[Construct]]是通过使用带new运算符的构造函数来激活的,正如我们所说的这个方法是负责内存分配和对象创建的。如果没有参数,调用构造函数的括号也可以省略:
复制代码 代码如下:
function A(x) { // constructor А
  this.x = x || 10;
}
 
// 不传参数的话,括号也可以省略
var a = new A; // or new A();
alert(a.x); // 10
 
// 显式传入参数x
var b = new A(20);
alert(b.x); // 20

我们也知道,构造函数(初始化阶段)里的shis被设置为新创建的对象 。

让我们研究一下对象创建的算法。

对象创建的算法

内部方法[[Construct]] 的行为可以描述成如下:
复制代码 代码如下:
F.[[Construct]](initialParameters):
 
O = new NativeObject();
 
// 属性[[Class]]被设置为"Object"
O.[[Class]] = "Object"
 
// 引用F.prototype的时候获取该对象g
var __objectPrototype = F.prototype;
 
// 如果__objectPrototype是对象,就:
O.[[Prototype]] = __objectPrototype
// 否则:
O.[[Prototype]] = Object.prototype;
// 这里O.[[Prototype]]是Object对象的原型
 
// 新创建对象初始化的时候应用了F.[[Call]]
// 将this设置为新创建的对象O
// 参数和F里的initialParameters是一样的
R = F.[[Call]](initialParameters); this === O;
// 这里R是[[Call]]的返回值
// 在JS里看,像这样:
// R = F.apply(O, initialParameters);
 
// 如果R是对象
return R
// 否则
return O

请注意两个主要特点:

1.首先,新创建对象的原型是从当前时刻函数的prototype属性获取的(这意味着同一个构造函数创建的两个创建对象的原型可以不同是因为函数的prototype属性也可以不同)。
2.其次,正如我们上面提到的,如果在对象初始化的时候,[[Call]]返回的是对象,这恰恰是用于整个new操作符的结果:
复制代码 代码如下:
function A() {}
A.prototype.x = 10;
 
var a = new A();
alert(a.x); // 10 – 从原型上得到
 
// 设置.prototype属性为新对象
// 为什么显式声明.constructor属性将在下面说明
A.prototype = {
  constructor: A,
  y: 100
};
 
var b = new A();
// 对象"b"有了新属性
alert(b.x); // undefined
alert(b.y); // 100 – 从原型上得到
 
// 但a对象的原型依然可以得到原来的结果
alert(a.x); // 10 - 从原型上得到
 
function B() {
  this.x = 10;
  return new Array();
}
 
// 如果"B"构造函数没有返回(或返回this)
// 那么this对象就可以使用,但是下面的情况返回的是array
var b = new B();
alert(b.x); // undefined
alert(Object.prototype.toString.call(b)); // [object Array]

让我们来详细了解一下原型

原型

每个对象都有一个原型(一些系统对象除外)。原型通信是通过内部的、隐式的、不可直接访问[[Prototype]]原型属性来进行的,原型可以是一个对象,也可以是null值。

属性构造函数(Property constructor)

上面的例子有有2个重要的知识点,第一个是关于函数的constructor属性的prototype属性,在函数创建的算法里,我们知道constructor属性在函数创建阶段被设置为函数的prototype属性,constructor属性的值是函数自身的重要引用:

复制代码 代码如下:
function A() {}
var a = new A();
alert(a.constructor); // function A() {}, by delegation
alert(a.constructor === A); // true

通常在这种情况下,存在着一个误区:constructor构造属性作为新创建对象自身的属性是错误的,但是,正如我们所看到的的,这个属性属于原型并且通过继承来访问对象。

通过继承constructor属性的实例,可以间接得到的原型对象的引用:
复制代码 代码如下:
function A() {}
A.prototype.x = new Number(10);
 
var a = new A();
alert(a.constructor.prototype); // [object Object]
 
alert(a.x); // 10, 通过原型
// 和a.[[Prototype]].x效果一样
alert(a.constructor.prototype.x); // 10
 
alert(a.constructor.prototype.x === a.x); // true

但请注意,函数的constructor和prototype属性在对象创建以后都可以重新定义的。在这种情况下,对象失去上面所说的机制。如果通过函数的prototype属性去编辑元素的prototype原型的话(添加新对象或修改现有对象),实例上将看到新添加的属性。

然而,如果我们彻底改变函数的prototype属性(通过分配一个新的对象),那原始构造函数的引用就是丢失,这是因为我们创建的对象不包括constructor属性:
复制代码 代码如下:
function A() {}
A.prototype = {
  x: 10
};
 
var a = new A();
alert(a.x); // 10
alert(a.constructor === A); // false!

因此,对函数的原型引用需要手工恢复:
复制代码 代码如下:
function A() {}
A.prototype = {
  constructor: A,
  x: 10
};
 
var a = new A();
alert(a.x); // 10
alert(a.constructor === A); // true

注意虽然手动恢复了constructor属性,和原来丢失的原型相比,{DontEnum}特性没有了,也就是说A.prototype里的for..in循环语句不支持了,不过第5版规范里,通过[[Enumerable]] 特性提供了控制可枚举状态enumerable的能力。
复制代码 代码如下:
var foo = {x: 10};
 
Object.defineProperty(foo, "y", {
  value: 20,
  enumerable: false // aka {DontEnum} = true
});
 
console.log(foo.x, foo.y); // 10, 20
 
for (var k in foo) {
  console.log(k); // only "x"
}
 
var xDesc = Object.getOwnPropertyDescriptor(foo, "x");
var yDesc = Object.getOwnPropertyDescriptor(foo, "y");
 
console.log(
  xDesc.enumerable, // true
  yDesc.enumerable  // false
);

显式prototype和隐式[[Prototype]]属性

通常,一个对象的原型通过函数的prototype属性显式引用是不正确的,他引用的是同一个对象,对象的[[Prototype]]属性:

a.[[Prototype]] ----> Prototype <---- A.prototype

此外, 实例的[[Prototype]]值确实是在构造函数的prototype属性上获取的。

然而,提交prototype属性不会影响已经创建对象的原型(只有在构造函数的prototype属性改变的时候才会影响到),就是说新创建的对象才有有新的原型,而已创建对象还是引用到原来的旧原型(这个原型已经不能被再被修改了)。
复制代码 代码如下:
// 在修改A.prototype原型之前的情况
a.[[Prototype]] ----> Prototype <---- A.prototype
 
// 修改之后
A.prototype ----> New prototype // 新对象会拥有这个原型
a.[[Prototype]] ----> Prototype // 引导的原来的原型上

例如:
复制代码 代码如下:
function A() {}
A.prototype.x = 10;
 
var a = new A();
alert(a.x); // 10
 
A.prototype = {
  constructor: A,
  x: 20
  y: 30
};
 
// 对象a是通过隐式的[[Prototype]]引用从原油的prototype上获取的值
alert(a.x); // 10
alert(a.y) // undefined
 
var b = new A();
 
// 但新对象是从新原型上获取的值
alert(b.x); // 20
alert(b.y) // 30

因此,有的文章说“动态修改原型将影响所有的对象都会拥有新的原型”是错误的,新原型仅仅在原型修改以后的新创建对象上生效。

这里的主要规则是:对象的原型是对象的创建的时候创建的,并且在此之后不能修改为新的对象,如果依然引用到同一个对象,可以通过构造函数的显式prototype引用,对象创建以后,只能对原型的属性进行添加或修改。

非标准的__proto__属性

然而,有些实现(例如SpiderMonkey),提供了不标准的__proto__显式属性来引用对象的原型:
复制代码 代码如下:
function A() {}
A.prototype.x = 10;
 
var a = new A();
alert(a.x); // 10
 
var __newPrototype = {
  constructor: A,
  x: 20,
  y: 30
};
 
// 引用到新对象
A.prototype = __newPrototype;
 
var b = new A();
alert(b.x); // 20
alert(b.y); // 30
 
// "a"对象使用的依然是旧的原型
alert(a.x); // 10
alert(a.y); // undefined
 
// 显式修改原型
a.__proto__ = __newPrototype;
 
// 现在"а"对象引用的是新对象
alert(a.x); // 20
alert(a.y); // 30

注意,ES5提供了Object.getPrototypeOf(O)方法,该方法直接返回对象的[[Prototype]]属性——实例的初始原型。 然而,和__proto__相比,它只是getter,它不允许set值。
复制代码 代码如下:
var foo = {};
Object.getPrototypeOf(foo) == Object.prototype; // true

对象独立于构造函数 因为实例的原型独立于构造函数和构造函数的prototype属性,构造函数完成了自己的主要工作(创建对象)以后可以删除。原型对象通过引用[[Prototype]]属性继续存在:
复制代码 代码如下:
function A() {}
A.prototype.x = 10;
 
var a = new A();
alert(a.x); // 10
 
// 设置A为null - 显示引用构造函数
A = null;
 
// 但如果.constructor属性没有改变的话,
// 依然可以通过它创建对象
var b = new a.constructor();
alert(b.x); // 10
 
// 隐式的引用也删除掉
delete a.constructor.prototype.constructor;
delete b.constructor.prototype.constructor;
 
// 通过A的构造函数再也不能创建对象了
// 但这2个对象依然有自己的原型
alert(a.x); // 10
alert(b.x); // 10

instanceof操作符的特性 我们是通过构造函数的prototype属性来显示引用原型的,这和instanceof操作符有关。该操作符是和原型链一起工作的,而不是构造函数,考虑到这一点,当检测对象的时候往往会有误解:
复制代码 代码如下:
if (foo instanceof Foo) {
  ...
}

这不是用来检测对象foo是否是用Foo构造函数创建的,所有instanceof运算符只需要一个对象属性——foo.[[Prototype]],在原型链中从Foo.prototype开始检查其是否存在。instanceof运算符是通过构造函数里的内部方法[[HasInstance]]来激活的。

让我们来看看这个例子:
复制代码 代码如下:
function A() {}
A.prototype.x = 10;
 
var a = new A();
alert(a.x); // 10
 
alert(a instanceof A); // true
 
// 如果设置原型为null
A.prototype = null;
 
// ..."a"依然可以通过a.[[Prototype]]访问原型
alert(a.x); // 10
 
// 不过,instanceof操作符不能再正常使用了
// 因为它是从构造函数的prototype属性来实现的
alert(a instanceof A); // 错误,A.prototype不是对象

另一方面,可以由构造函数来创建对象,但如果对象的[[Prototype]]属性和构造函数的prototype属性的值设置的是一样的话,instanceof检查的时候会返回true:
复制代码 代码如下:
function B() {}
var b = new B();
 
alert(b instanceof B); // true
 
function C() {}
 
var __proto = {
  constructor: C
};
 
C.prototype = __proto;
b.__proto__ = __proto;
 
alert(b instanceof C); // true
alert(b instanceof B); // false

原型可以存放方法并共享属性 大部分程序里使用原型是用来存储对象的方法、默认状态和共享对象的属性。

事实上,对象可以拥有自己的状态 ,但方法通常是一样的。 因此,为了内存优化,方法通常是在原型里定义的。 这意味着,这个构造函数创建的所有实例都可以共享找个方法。
复制代码 代码如下:
function A(x) {
  this.x = x || 100;
}
 
A.prototype = (function () {
 
  // 初始化上下文
  // 使用额外的对象
 
  var _someSharedVar = 500;
 
  function _someHelper() {
    alert('internal helper: ' _someSharedVar);
  }
 
  function method1() {
    alert('method1: ' this.x);
  }
 
  function method2() {
    alert('method2: ' this.x);
    _someHelper();
  }
 
  // 原型自身
  return {
    constructor: A,
    method1: method1,
    method2: method2
  };
 
})();
 
var a = new A(10);
var b = new A(20);
 
a.method1(); // method1: 10
a.method2(); // method2: 10, internal helper: 500
 
b.method1(); // method1: 20
b.method2(); // method2: 20, internal helper: 500
 
// 2个对象使用的是原型里相同的方法
alert(a.method1 === b.method1); // true
alert(a.method2 === b.method2); // true

读写属性

正如我们提到,读取和写入属性值是通过内部的[[Get]]和[[Put]]方法。这些内部方法是通过属性访问器激活的:点标记法或者索引标记法:
复制代码 代码如下:
// 写入
foo.bar = 10; // 调用了[[Put]]
 
console.log(foo.bar); // 10, 调用了[[Get]]
console.log(foo['bar']); // 效果一样

让我们用伪代码来看一下这些方法是如何工作的:

[[Get]]方法

[[Get]]也会从原型链中查询属性,所以通过对象也可以访问原型中的属性。

O.[[Get]](P):
复制代码 代码如下:
// 如果是自己的属性,就返回
if (O.hasOwnProperty(P)) {
  return O.P;
}
 
// 否则,继续分析原型
var __proto = O.[[Prototype]];
 
// 如果原型是null,返回undefined
// 这是可能的:最顶层Object.prototype.[[Prototype]]是null
if (__proto === null) {
  return undefined;
}
 
// 否则,对原型链递归调用[[Get]],在各层的原型中查找属性
// 直到原型为null
return __proto.[[Get]](P)

请注意,因为[[Get]]在如下情况也会返回undefined:
复制代码 代码如下:
if (window.someObject) {
  ...
}

这里,在window里没有找到someObject属性,然后会在原型里找,原型的原型里找,以此类推,如果都找不到,按照定义就返回undefined。

注意:in操作符也可以负责查找属性(也会查找原型链):
复制代码 代码如下:
if ('someObject' in window) {
  ...
}

这有助于避免一些特殊问题:比如即便someObject存在,在someObject等于false的时候,第一轮检测就通不过。

[[Put]]方法

[[Put]]方法可以创建、更新对象自身的属性,并且掩盖原型里的同名属性。

O.[[Put]](P, V):
复制代码 代码如下:
// 如果不能给属性写值,就退出
if (!O.[[CanPut]](P)) {
  return;
}
 
// 如果对象没有自身的属性,就创建它
// 所有的attributes特性都是false
if (!O.hasOwnProperty(P)) {
  createNewProperty(O, P, attributes: {
    ReadOnly: false,
    DontEnum: false,
    DontDelete: false,
    Internal: false
  });
}
 
// 如果属性存在就设置值,但不改变attributes特性
O.P = V
 
return;

例如:
复制代码 代码如下:
Object.prototype.x = 100;
 
var foo = {};
console.log(foo.x); // 100, 继承属性
 
foo.x = 10; // [[Put]]
console.log(foo.x); // 10, 自身属性
 
delete foo.x;
console.log(foo.x); // 重新是100,继承属性
请注意,不能掩盖原型里的只读属性,赋值结果将忽略,这是由内部方法[[CanPut]]控制的。

// 例如,属性length是只读的,我们来掩盖一下length试试
 
function SuperString() {
  /* nothing */
}
 
SuperString.prototype = new String("abc");
 
var foo = new SuperString();
 
console.log(foo.length); // 3, "abc"的长度
 
// 尝试掩盖
foo.length = 5;
console.log(foo.length); // 依然是3

但在ES5的严格模式下,如果掩盖只读属性的话,会保存TypeError错误。

属性访问器

内部方法[[Get]]和[[Put]]在ECMAScript里是通过点符号或者索引法来激活的,如果属性标示符是合法的名字的话,可以通过“.”来访问,而索引方运行动态定义名称。
复制代码 代码如下:
var a = {testProperty: 10};
 
alert(a.testProperty); // 10, 点
alert(a['testProperty']); // 10, 索引
 
var propertyName = 'Property';
alert(a['test' propertyName]); // 10, 动态属性通过索引的方式

这里有一个非常重要的特性——属性访问器总是使用ToObject规范来对待“.”左边的值。这种隐式转化和这句“在JavaScript中一切都是对象”有关系,(然而,当我们已经知道了,JavaScript里不是所有的值都是对象)。

如果对原始值进行属性访问器取值,访问之前会先对原始值进行对象包装(包括原始值),然后通过包装的对象进行访问属性,属性访问以后,包装对象就会被删除。

例如:
复制代码 代码如下:
var a = 10; // 原始值
 
// 但是可以访问方法(就像对象一样)
alert(a.toString()); // "10"
 
// 此外,我们可以在a上创建一个心属性
a.test = 100; // 好像是没问题的
 
// 但,[[Get]]方法没有返回该属性的值,返回的却是undefined
alert(a.test); // undefined

那么,为什么整个例子里的原始值可以访问toString方法,而不能访问新创建的test属性呢?

答案很简单:

首先,正如我们所说,使用属性访问器以后,它已经不是原始值了,而是一个包装过的中间对象(整个例子是使用new Number(a)),而toString方法这时候是通过原型链查找到的:
复制代码 代码如下:
// 执行a.toString()的原理:  

  1. wrapper = new Number(a);
  2. wrapper.toString(); // "10"
  3. delete wrapper;

接下来,[[Put]]方法创建新属性时候,也是通过包装装的对象进行的:
复制代码 代码如下:
// 执行a.test = 100的原理:  

  1. wrapper = new Number(a);
  2. wrapper.test = 100;
  3. delete wrapper;

我们看到,在第3步的时候,包装的对象以及删除了,随着新创建的属性页被删除了——删除包装对象本身。

然后使用[[Get]]获取test值的时候,再一次创建了包装对象,但这时候包装的对象已经没有test属性了,所以返回的是undefined:
复制代码 代码如下:
// 执行a.test的原理:  

  1. wrapper = new Number(a);
  2. wrapper.test; // undefined

这种方式解释了原始值的读取方式,另外,任何原始值如果经常用在访问属性的话,时间效率考虑,都是直接用一个对象替代它;与此相反,如果不经常访问,或者只是用于计算的话,到可以保留这种形式。

继承

我们知道,ECMAScript是使用基于原型的委托式继承。链和原型在原型链里已经提到过了。其实,所有委托的实现和原型链的查找分析都浓缩到[[Get]]方法了。

如果你完全理解[[Get]]方法,那JavaScript中的继承这个问题将不解自答了。

经常在论坛上谈论JavaScript中的继承时,我都是用一行代码来展示,事实上,我们不需要创建任何对象或函数,因为该语言已经是基于继承的了,代码如下:
复制代码 代码如下:
alert(1..toString()); // "1"

我们已经知道了[[Get]]方法和属性访问器的原理了,我们来看看都发生了什么:

1.首先,从原始值1,通过new Number(1)创建包装对象
2.然后toString方法是从这个包装对象上继承得到的

为什么是继承的? 因为在ECMAScript中的对象可以有自己的属性,包装对象在这种情况下没有toString方法。 因此它是从原理里继承的,即Number.prototype。

注意有个微妙的地方,在上面的例子中的两个点不是一个错误。第一点是代表小数部分,第二个才是一个属性访问器:
复制代码 代码如下:
1.toString(); // 语法错误!
 
(1).toString(); // OK
 
1..toString(); // OK
 
1['toString'](); // OK

原型链

让我们展示如何为用户定义对象创建原型链,非常简单:
复制代码 代码如下:
function A() {
  alert('A.[[Call]] activated');
  this.x = 10;
}
A.prototype.y = 20;
 
var a = new A();
alert([a.x, a.y]); // 10 (自身), 20 (继承)
 
function B() {}
 
// 最近的原型链方式就是设置对象的原型为另外一个新对象
B.prototype = new A();
 
// 修复原型的constructor属性,否则的话是A了
B.prototype.constructor = B;
 
var b = new B();
alert([b.x, b.y]); // 10, 20, 2个都是继承的
 
// [[Get]] b.x:
// b.x (no) -->
// b.[[Prototype]].x (yes) - 10
 
// [[Get]] b.y
// b.y (no) -->
// b.[[Prototype]].y (no) -->
// b.[[Prototype]].[[Prototype]].y (yes) - 20
 
// where b.[[Prototype]] === B.prototype,
// and b.[[Prototype]].[[Prototype]] === A.prototype

这种方法有两个特性:

首先,B.prototype将包含x属性。乍一看这可能不对,你可能会想x属性是在A里定义的并且B构造函数也是这样期望的。尽管原型继承正常情况是没问题的,但B构造函数有时候可能不需要x属性,与基于class的继承相比,所有的属性都复制到后代子类里了。

尽管如此,如果有需要(模拟基于类的继承)将x属性赋给B构造函数创建的对象上,有一些方法,我们后来来展示其中一种方式。

其次,这不是一个特征而是缺点——子类原型创建的时候,构造函数的代码也执行了,我们可以看到消息"A.[[Call]] activated"显示了两次——当用A构造函数创建对象赋给B.prototype属性的时候,另外一场是a对象创建自身的时候!

下面的例子比较关键,在父类的构造函数抛出的异常:可能实际对象创建的时候需要检查吧,但很明显,同样的case,也就是就是使用这些父对象作为原型的时候就会出错。
复制代码 代码如下:
function A(param) {
  if (!param) {
    throw 'Param required';
  }
  this.param = param;
}
A.prototype.x = 10;
 
var a = new A(20);
alert([a.x, a.param]); // 10, 20
 
function B() {}
B.prototype = new A(); // Error

此外,在父类的构造函数有太多代码的话也是一种缺点。

解决这些“功能”和问题,程序员使用原型链的标准模式(下面展示),主要目的就是在中间包装构造函数的创建,这些包装构造函数的链里包含需要的原型。
复制代码 代码如下:
function A() {
  alert('A.[[Call]] activated');
  this.x = 10;
}
A.prototype.y = 20;
 
var a = new A();
alert([a.x, a.y]); // 10 (自身), 20 (集成)
 
function B() {
  // 或者使用A.apply(this, arguments)
  B.superproto.constructor.apply(this, arguments);
}
 
// 继承:通过空的中间构造函数将原型连在一起
var F = function () {};
F.prototype = A.prototype; // 引用
B.prototype = new F();
B.superproto = A.prototype; // 显示引用到另外一个原型上, "sugar"
 
// 修复原型的constructor属性,否则的就是A了
B.prototype.constructor = B;
 
var b = new B();
alert([b.x, b.y]); // 10 (自身), 20 (集成)

注意,我们在b实例上创建了自己的x属性,通过B.superproto.constructor调用父构造函数来引用新创建对象的上下文。

我们也修复了父构造函数在创建子原型的时候不需要的调用,此时,消息"A.[[Call]] activated"在需要的时候才会显示。

为了在原型链里重复相同的行为(中间构造函数创建,设置superproto,恢复原始构造函数),下面的模板可以封装成一个非常方面的工具函数,其目的是连接原型的时候不是根据构造函数的实际名称。
复制代码 代码如下:
function inherit(child, parent) {
  var F = function () {};
  F.prototype = parent.prototype
  child.prototype = new F();
  child.prototype.constructor = child;
  child.superproto = parent.prototype;
  return child;
}

因此,继承:
复制代码 代码如下:
function A() {}
A.prototype.x = 10;
 
function B() {}
inherit(B, A); // 连接原型
 
var b = new B();
alert(b.x); // 10, 在A.prototype查找到

也有很多语法形式(包装而成),但所有的语法行都是为了减少上述代码里的行为。

例如,如果我们把中间的构造函数放到外面,就可以优化前面的代码(因此,只有一个函数被创建),然后重用它:
复制代码 代码如下:
var inherit = (function(){
  function F() {}
  return function (child, parent) {
    F.prototype = parent.prototype;
    child.prototype = new F;
    child.prototype.constructor = child;
    child.superproto = parent.prototype;
    return child;
  };
})();

由于对象的真实原型是[[Prototype]]属性,这意味着F.prototype可以很容易修改和重用,因为通过new F创建的child.prototype可以从child.prototype的当前值里获取[[Prototype]]:
复制代码 代码如下:
function A() {}
A.prototype.x = 10;
 
function B() {}
inherit(B, A);
 
B.prototype.y = 20;
 
B.prototype.foo = function () {
  alert("B#foo");
};
 
var b = new B();
alert(b.x); // 10, 在A.prototype里查到
 
function C() {}
inherit(C, B);
 
// 使用"superproto"语法糖
// 调用父原型的同名方法
 
C.ptototype.foo = function () {
  C.superproto.foo.call(this);
  alert("C#foo");
};
 
var c = new C();
alert([c.x, c.y]); // 10, 20
 
c.foo(); // B#foo, C#foo

注意,ES5为原型链标准化了这个工具函数,那就是Object.create方法。ES3可以使用以下方式实现:
复制代码 代码如下:
Object.create ||
Object.create = function (parent, properties) {
  function F() {}
  F.prototype = parent;
  var child = new F;
  for (var k in properties) {
    child[k] = properties[k].value;
  }
  return child;
}

// 用法
var foo = {x: 10};
var bar = Object.create(foo, {y: {value: 20}});
console.log(bar.x, bar.y); // 10, 20

此外,所有模仿现在基于类的经典继承方式都是根据这个原则实现的,现在可以看到,它实际上不是基于类的继承,而是连接原型的一个很方便的代码重用。

结论

本章内容已经很充分和详细了,希望这些资料对你有用,并且消除你对ECMAScript的疑问,如果你有任何问题,请留言,我们一起讨论。

介绍 本章是关于ECMAScript面向对象实现的第2篇,第1篇我们讨论的是概论和CEM...

介绍

我们知道 instanceof 运算符用来检查对象是否为某构造器的实例。下面列举它返回true的各种情景。

 

本章是关于ECMAScript面向对象实现的第2篇,第1篇我们讨论的是概论和CEMAScript的比较,如果你还没有读第1篇,在进行本章之前,我强烈建议你先读一下第1篇,因为本篇实在太长了(35页)。

1、对象obj是通过new Constructor创建的,那么 obj instanceof Constructor 为true

prototype是javascript实现与管理继承的一种机制,也是面向对象的设计思想.构造函数的原型存储着引用对象的一个指针,该指针指向与一个原型对象,对象内部存储着函数的原始属性和方法;我们可以借助prototype属性,可以访问原型内部的属性和方法。

英文原文:
注:由于篇幅太长了,难免出现错误,时刻保持修正中。

复制代码 代码如下:

 

在概论里,我们延伸到了ECMAScript,现在,当我们知道它OOP实现时,我们再来准确定义一下:

function Person(n, a) {
    this.name = n;
    this.age = a;
}
var p = new Person('John Backus', 82);
console.log(p instanceof Person); // true

当构造函数被实列化后,所有的实例对象都可以访问构造函数的原型成员,如果在原型中声明一个成员,所有的实列方法都可以共享它,比如如下代码:

复制代码 代码如下:

2、如果存在继承关系,那么 子类实例 instanceof 父类 也会返回true

 

ECMAScript is an object-oriented programming language supporting delegating inheritance based on prototypes.

复制代码 代码如下:

// 构造函数A 它的原型有一个getName方法

function A(name){

this.name = name;

}

A.prototype.getName = function(){

return this.name;

}

// 实列化2次后 该2个实列都有原型getName方法;如下代码

var instance1 = new A("longen1");

var instance2 = new A("longen2");

console.log(instance1.getName()); //longen1

console.log(instance2.getName()); // longen2

ECMAScript是一种面向对象语言,支持基于原型的委托式继承。
我们将从最基本的数据类型来分析,首先要了解的是ECMAScript用原始值(primitive values)和对象(objects)来区分实体,因此有些文章里说的“在JavaScript里,一切都是对象”是错误的(不完全对),原始值就是我们这里要讨论的一些数据类型。

function A(){}
function B(){}
B.prototype = new A(); // B继承于A

 

数据类型

var b = new B();
console.log(b instanceof A); // true

原型具有普通对象结构,可以将任何普通对象设置为原型对象; 一般情况下,对象都继承与Object,也可以理解Object是所有对象的超类,Object是没有原型的,而构造函数拥有原型,因此实列化的对象也是Object的实列,如下代码:

虽然ECMAScript是可以动态转化类型的动态弱类型语言,它还是有数据类型的。也就是说,一个对象要属于一个实实在在的类型。
标准规范里定义了9种数据类型,但只有6种是在ECMAScript程序里可以直接访问的,它们是:Undefined、Null、Boolean、String、Number、Object。

3、由于Object是根类,所有其它自定义类都继承于它,因此 任意构造器的实例 instanceof Object 都返回true

 

另外3种类型只能在实现级别访问(ECMAScript对象是不能使用这些类型的)并用于规范来解释一些操作行为、保存中间值。这3种类型是:Reference、List和Completion。

复制代码 代码如下:

// 实列化对象是构造函数的实列

console.log(instance1 instanceof A); //true

console.log(instance2 instanceof A); // true

 

// 实列化对象也是Object的实列

console.log(instance1 instanceof Object); //true

console.log(instance2 instanceof Object); //true

 

//Object 对象是所有对象的超类,因此构造函数也是Object的实列

console.log(A instanceof Object); // true

 

// 但是实列化对象 不是Function对象的实列 如下代码

console.log(instance1 instanceof Function); // false

console.log(instance2 instanceof Function); // false

 

// 但是Object与Function有关系 如下代码说明

console.log(Function instanceof Object); // true

console.log(Object instanceof Function); // true

因此,Reference是用来解释delete、typeof、this这样的操作符,并且包含一个基对象和一个属性名称;List描述的是参数列表的行为(在new表达式和函数调用的时候);Completion是用来解释行为break、continue、return和throw语句的。

function A() {}
var a = new A();
console.log(a instanceof Object); // true

 

原始值类型 回头来看6中用于ECMAScript程序的数据类型,前5种是原始值类型,包括Undefined、Null、Boolean、String、Number、Object。
原始值类型例子:

var str = new String('hello');
console.log(str instanceof Object); // true

如上代码,Function是Object的实列,也可以是Object也是Function的实列;他们是2个不同的构造器,我们继续看如下代码:

复制代码 代码如下:

var num = new Number(1);
console.log(num instanceof Object); // true

 

var a = undefined;
var b = null;
var c = true;
var d = 'test';
var e = 10;

甚至包括构造器自身

var f = new Function();

var o = new Object();

console.log("------------");

console.log(f instanceof Function); //true

console.log(o instanceof Function); // false

console.log(f instanceof Object); // true

console.log(o instanceof Object); // true

这些值是在底层上直接实现的,他们不是object,所以没有原型,没有构造函数。

复制代码 代码如下:

 

大叔注:这些原生值和我们平时用的(Boolean、String、Number、Object)虽然名字上相似,但不是同一个东西。所以typeof(true)和typeof(Boolean)结果是不一样的,因为typeof(Boolean)的结果是function,所以函数Boolean、String、Number是有原型的(下面的读写属性章节也会提到)。

function A() {}
console.log(A instanceof Object); // true
console.log(String instanceof Object); // true
console.log(Number instanceof Object); // true

我们明白,在原型上增加成员属性或者方法的话,它被所有的实列化对象所共享属性和方法,但是如果实列化对象有和原型相同的成员成员名字的话,那么它取到的成员是本实列化对象,如果本实列对象中没有的话,那么它会到原型中去查找该成员,如果原型找到就返回,否则的会返回undefined,如下代码测试

想知道数据是哪种类型用typeof是最好不过了,有个例子需要注意一下,如果用typeof来判断null的类型,结果是object,为什么呢?因为null的类型是定义为Null的。

4、所有构造器 instanceof Function 返回true

 

复制代码 代码如下:

复制代码 代码如下:

function B(){

this.name = "longen2";

}

B.prototype.name = "AA";

B.prototype.getName = function(){

return this.name;

};

 

var b1 = new B();

// 在本实列查找,找到就返回,否则到原型查找

console.log(b1.name); // longen2

 

// 在本实列没有找到该方法,就到原型去查找

console.log(b1.getName());//longen2

 

// 如果在本实列没有找到的话,到原型上查找也没有找到的话,就返回undefined

console.log(b1.a); // undefined

 

// 现在我使用delete运算符删除本地实列属性,那么取到的是就是原型属性了,如下代码:

delete b1.name;

console.log(b1.name); // AA

alert(typeof null); // "object"

function A() {}
console.log(A instanceof Function); // true
console.log(String instanceof Function); // true
console.log(Number instanceof Function); // true

 

显示"object"原因是因为规范就是这么规定的:对于Null值的typeof字符串值返回"object“。

以上四点总结为一句话:如果某实例是通过某类或其子类的创建的,那么instanceof就返回true。或者说某构造函数的原型 存在与对象obj的内部原型链上,那么返回true。即instanceof的结果与构造器自身并无直接关系。这在许多语言中都是通用的。

二:理解原型域链的概念

规范没有想象解释这个,但是Brendan Eich (JavaScript发明人)注意到null相对于undefined大多数都是用于对象出现的地方,例如设置一个对象为空引用。但是有些文档里有些气人将之归结为bug,而且将该bug放在Brendan Eich也参与讨论的bug列表里,结果就是任其自然,还是把typeof null的结果设置为object(尽管262-3的标准是定义null的类型是Null,262-5已经将标准修改为null的类型是object了)。

Java中定义了一个类Person,实例p对于Person和Object都返回true

 

Object类型

复制代码 代码如下:

原型的优点是能够以对象结构为载体,创建大量的实列,这些实列能共享原型中的成员(属性和方法);同时也可以使用原型实现面向对象中的继承机制~ 如下代码:下面我们来看这个构造函数AA和构造函数BB,当BB.prototype = new AA(11);执行这个的时候,那么B就继承与A,B中的原型就有x的属性值为11

接着,Object类型(不要和Object构造函数混淆了,现在只讨论抽象类型)是描述 ECMAScript对象的唯一一个数据类型。

class Person {
    public String name;
    public int age;
    Person (String n, int a) {
        this.name = name;
        this.age = a;
    }
    public static void main(String[] args) {
        Person p = new Person("John Backus", 82);
        System.out.println(p instanceof Person); // true
        System.out.println(p instanceof Object); // true
    }
}

 

Object is an unordered collection of key-value pairs.
对象是一个包含key-value对的无序集合

Java中如果存在继承关系,那么 子类实例 instanceof 父类 也返回true

function AA(x){

this.x = x;

}

function BB(x) {

this.x = x;

}

BB.prototype = new AA(11);

console.log(BB.prototype.x); //11

 

// 我们再来理解原型继承和原型链的概念,代码如下,都有注释

function A(x) {

this.x = x;

}

// 在A的原型上定义一个属性x = 0

A.prototype.x = 0;

function B(x) {

this.x = x;

}

B.prototype = new A(1);

对象的key值被称为属性,属性是原始值和其他对象的容器。如果属性的值是函数我们称它为方法 。

复制代码 代码如下:

 

例如:

// 父类
class Person {
    public String name;
    public int age;
    Person (String n, int a) {
        name = name;
        age = a;
    }
}
// 子类
public class Man extends Person{
    public String university;
    Man(String n, int a, String s) {
        super(n, a);
        university = s;
    }
    public static void main(String[] args) {
        Man mm = new Man("John Resig", 29, "PKU");
        System.out.println(mm instanceof Man); // true
        System.out.println(mm instanceof Person); // 也是true
    }
}

实列化A new A(1)的时候 在A函数内this.x =1, B.prototype = new A(1);B.prototype 是A的实列 也就是B继承于A, 即B.prototype.x = 1; 如下代码:

复制代码 代码如下:

知道了这些,JS中以下的表现就不奇怪了

 

var x = { // 对象"x"有3个属性: a, b, c
  a: 10, // 原始值
  b: {z: 100}, // 对象"b"有一个属性z
  c: function () { // 函数(方法)
    alert('method x.c');
  }
};
 
alert(x.a); // 10
alert(x.b); // [object Object]
alert(x.b.z); // 100
x.c(); // 'method x.c'

复制代码 代码如下:

console.log(B.prototype.x); // 1

// 定义C的构造函数

function C(x) {

this.x = x;

}

C.prototype = new B(2);

动态性

// 定义两个构造器
function A(){}
function B(){}
A.prototype = B.prototype = {a: 1};

 

正如我们在第17章中指出的,ES中的对象是完全动态的。这意味着,在程序执行的时候我们可以任意地添加,修改或删除对象的属性。

// 分别创建两个不同构造器的实例
var a = new A();
var b = new B();
console.log(a instanceof B); // true
console.log(b instanceof A); // true

C.prototype = new B(2); 也就是C.prototype 是B的实列,C继承于B;那么new B(2)的时候 在B的构造函数内 this.x = 2;那么 C的原型上会有一个属性x =2 即C.prototype.x = 2; 如下代码:

例如:

我们看到a, b分别是用A和B创建的,但a instanceof B和 b instanceof A都是true。即a虽然不是用构造器B创建的,但仍然返回true。因为B.prototype存在于a的内部原型链上。

 

复制代码 代码如下:

由于JS的动态语言特性,可以在运行时修改原型,因此下面返回false也不足为奇了。因为A.prototype已经不在a的内部原型链中,链条被打断了。

console.log(C.prototype.x); // 2

var foo = {x: 10};
 
// 添加新属性
foo.y = 20;
console.log(foo); // {x: 10, y: 20}
 
// 将属性值修改为函数
foo.x = function () {
  console.log('foo.x');
};
 
foo.x(); // 'foo.x'
 
// 删除属性
delete foo.x;
console.log(foo); // {y: 20}

复制代码 代码如下:

 

有些属性不能被修改——(只读属性、已删除属性或不可配置的属性)。 我们将稍后在属性特性里讲解。

function A(){}
var a = new A();
A.prototype = {}; // 动态修改原型,注意必须在创建a后
console.log(a instanceof A); // false

下面是实列化 var d = new C(3); 实列化C的构造函数时候,那么在C的构造函数内this.x = 3; 因此如下打印实列化后的d.x = 3;如下代码:

另外,ES5规范规定,静态对象不能扩展新的属性,并且它的属性页不能删除或者修改。他们是所谓的冻结对象,可以通过应用Object.freeze(o)方法得到。

注意这么写也打破了上面总结的第一条:对象obj是通过new Constructor创建的,那么obj instanceof Constructor 为true

 

复制代码 代码如下:

实际在ECMAScript标准中(以5.1为准),instanceof 内部实现会调用构造器的内部方法[[HasInstance]],描述如下

var d = new C(3);

console.log(d.x); // 3

var foo = {x: 10};
 
// 冻结对象
Object.freeze(foo);
console.log(Object.isFrozen(foo)); // true
 
// 不能修改
foo.x = 100;
 
// 不能扩展
foo.y = 200;
 
// 不能删除
delete foo.x;
 
console.log(foo); // {x: 10}

图片 2

 

在ES5规范里,也使用Object.preventExtensions(o)方法防止扩展,或者使用Object.defineProperty(o)方法来定义属性:

假如F是一个函数对象,当F(V)执行时,以下步骤将发生:

删除d.x 再访问d.x的时候 本实列对象被删掉,只能从原型上去查找;由于C.prototype = new B(2); 也就是C继承于B,因此C的原型也有x = 2;即C.prototype.x = 2; 如下代码:

复制代码 代码如下:

1、如果instanceof左运算元V不是对象类型,直接返回false

 

var foo = {x : 10};
 
Object.defineProperty(foo, "y", {
  value: 20,
  writable: false, // 只读
  configurable: false // 不可配置
});
 
// 不能修改
foo.y = 200;
 
// 不能删除
delete foo.y; // false
 
// 防治扩展
Object.preventExtensions(foo);
console.log(Object.isExtensible(foo)); // false
 
// 不能添加新属性
foo.z = 30;
 
console.log(foo); {x: 10, y: 20}

复制代码 代码如下:

delete d.x;

console.log(d.x); //2

内置对象、原生对象及宿主对象

var a, b = 1, c = true, d = 'hello';
console.log(a instanceof Object); // false 这里a值为undefined
console.log(b instanceof Object); // false
console.log(c instanceof Object); // false
console.log(d instanceof Object); // false

 

有必要需要注意的是规范还区分了这内置对象、元素对象和宿主对象。

2/3、取构造器F的prototype属性,如果不是对象类型,须抛出TypeError异常,

删除C.prototype.x后,我们从上面代码知道,C是继承于B的,自身的原型被删掉后,会去查找父元素的原型链,因此在B的原型上找到x =1; 如下代码:

内置对象和元素对象是被ECMAScript规范定义和实现的,两者之间的差异微不足道。所有ECMAScript实现的对象都是原生对象(其中一些是内置对象、一些在程序执行的时候创建,例如用户自定义对象)。内置对象是原生对象的一个子集、是在程序开始之前内置到ECMAScript里的(例如,parseInt, Match等)。所有的宿主对象是由宿主环境提供的,通常是浏览器,并可能包括如window、alert等。

复制代码 代码如下:

 

注意,宿主对象可能是ES自身实现的,完全符合规范的语义。从这点来说,他们能称为“原生宿主”对象(尽快很理论),不过规范没有定义“原生宿主”对象的概念。

function A(){}
A.prototype = 1; // A的prototype设为非对象类型
var a = new A();
console.log(a instanceof A);

delete C.prototype.x;

console.log(d.x); // 1

Boolean,String和Number对象

各浏览器抛出的异常提示不同,

 

另外,规范也定义了一些原生的特殊包装类,这些对象是:

Firefox18:

当删除B的原型属性x后,由于B是继承于A的,因此会从父元素的原型链上查找A原型上是否有x的属性,如果有的话,就返回,否则看A是否有继承,没有继承的话,继续往Object上去查找,如果没有找到就返回undefined 因此当删除B的原型x后,delete B.prototype.x; 打印出A上的原型x=0; 如下代码:

1.布尔对象
2.字符串对象
3.数字对象

图片 3

 

这些对象的创建,是通过相应的内置构造器创建,并且包含原生值作为其内部属性,这些对象可以转换省原始值,反之亦然。

Chrome24:

delete B.prototype.x;

console.log(d.x); // 0

 

// 继续删除A的原型x后 结果没有找到,就返回undefined了;

delete A.prototype.x;

console.log(d.x); // undefined

复制代码 代码如下:

图片 4

 

var c = new Boolean(true);
var d = new String('test');
var e = new Number(10);
 
// 转换成原始值
// 使用不带new关键字的函数
с = Boolean(c);
d = String(d);
e = Number(e);
 
// 重新转换成对象
с = Object(c);
d = Object(d);
e = Object(e);

Safari6:

在javascript中,一切都是对象,Function和Object都是函数的实列;构造函数的父原型指向于Function原型,Function.prototype的父原型指向与Object的原型,Object的父原型也指向与Function原型,Object.prototype是所有原型的顶层;

此外,也有对象是由特殊的内置构造函数创建: Function(函数对象构造器)、Array(数组构造器) RegExp(正则表达式构造器)、Math(数学模块)、 Date(日期的构造器)等等,这些对象也是Object对象类型的值,他们彼此的区别是由内部属性管理的,我们在下面讨论这些内容。

图片 5

 

字面量Literal

Opera12:

如下代码:

对于三个对象的值:对象(object),数组(array)和正则表达式(regular expression),他们分别有简写的标示符称为:对象初始化器、数组初始化器、和正则表达式初始化器:

图片 6

 

复制代码 代码如下:

IE10:

Function.prototype.a = function(){

console.log("我是父原型Function");

}

Object.prototype.a = function(){

console.log("我是 父原型Object");

}

function A(){

this.a = "a";

}

A.prototype = {

B: function(){

console.log("b");

}

}

// Function 和 Object都是函数的实列 如下:

console.log(A instanceof Function); // true

console.log(A instanceof Object); // true

 

// A.prototype是一个对象,它是Object的实列,但不是Function的实列

console.log(A.prototype instanceof Function); // false

console.log(A.prototype instanceof Object); // true

 

// Function是Object的实列 同是Object也是Function的实列

console.log(Function instanceof Object); // true

console.log(Object instanceof Function); // true

 

/*

* Function.prototype是Object的实列 但是Object.prototype不是Function的实列

* 说明Object.prototype是所有父原型的顶层

*/

console.log(Function.prototype instanceof Object); //true

console.log(Object.prototype instanceof Function); // false

// 等价于new Array(1, 2, 3);
// 或者array = new Array();
// array[0] = 1;
// array[1] = 2;
// array[2] = 3;
var array = [1, 2, 3];
 
// 等价于
// var object = new Object();
// object.a = 1;
// object.b = 2;
// object.c = 3;
var object = {a: 1, b: 2, c: 3};
 
// 等价于new RegExp("^\d $", "g")
var re = /^d $/g;

图片 7  

 

注意,如果上述三个对象进行重新赋值名称到新的类型上的话,那随后的实现语义就是按照新赋值的类型来使用,例如在当前的Rhino和老版本SpiderMonkey 1.7的实现上,会成功以new关键字的构造器来创建对象,但有些实现(当前Spider/TraceMonkey)字面量的语义在类型改变以后却不一定改变。

4、不断的执行以下逻辑:将V设为内部原型的V,如果V是null则返回false,如果V和O都指向同一个对象,则返回true。

三:理解原型继承机制

复制代码 代码如下:

您可能感兴趣的文章:

  • 关于javascript中的typeof和instanceof介绍
  • JavaScript中instanceof运算符的用法总结
  • javascript之typeof、instanceof操作符使用探讨
  • JavaScript instanceof 的使用方法示例介绍
  • 谈谈我对JavaScript中typeof和instanceof的深入理解
  • JavaScript类型检测之typeof 和 instanceof 的缺陷与优化
  • 浅谈javascript中的instanceof和typeof
  • JavaScript必知必会(六) delete in instanceof
  • JavaScript中instanceof运算符的使用示例
  • 实例讲解JavaScript中instanceof运算符的用法
  • JavaScript的instanceof运算符学习教程

 

var getClass = Object.prototype.toString;
 
Object = Number;
 
var foo = new Object;
alert([foo, getClass.call(foo)]); // 0, "[object Number]"
 
var bar = {};
 
// Rhino, SpiderMonkey 1.7中 - 0, "[object Number]"
// 其它: still "[object Object]", "[object Object]"
alert([bar, getClass.call(bar)]);
 
// Array也是一样的效果
Array = Number;
 
foo = new Array;
alert([foo, getClass.call(foo)]); // 0, "[object Number]"
 
bar = [];
 
// Rhino, SpiderMonkey 1.7中 - 0, "[object Number]"
// 其它: still "", "[object Object]"
alert([bar, getClass.call(bar)]);
 
// 但对RegExp,字面量的语义是不被改变的。 semantics of the literal
// isn't being changed in all tested implementations
 
RegExp = Number;
 
foo = new RegExp;
alert([foo, getClass.call(foo)]); // 0, "[object Number]"
 
bar = /(?!)/g;
alert([bar, getClass.call(bar)]); // /(?!)/g, "[object RegExp]"

构造函数都有一个指针指向原型,Object.prototype是所有原型对象的顶层,比如如下代码:

正则表达式字面量和RegExp对象

 

注意,下面2个例子在第三版的规范里,正则表达式的语义都是等价的,regexp字面量只在一句里存在,并且再解析阶段创建,但RegExp构造器创建的却是新对象,所以这可能会导致出一些问题,如lastIndex的值在测试的时候结果是错误的:

var obj = {};

Object.prototype.name = "tugenhua";

console.log(obj.name); // tugenhua

复制代码 代码如下:

 

for (var k = 0; k < 4; k ) {
  var re = /ecma/g;
  alert(re.lastIndex); // 0, 4, 0, 4
  alert(re.test("ecmascript")); // true, false, true, false
}
 
// 对比
 
for (var k = 0; k < 4; k ) {
  var re = new RegExp("ecma", "g");
  alert(re.lastIndex); // 0, 0, 0, 0
  alert(re.test("ecmascript")); // true, true, true, true
}

给Object.prototype 定义一个属性,通过字面量构建的对象的话,都会从父类那边获取Object.prototype的属性;

注:不过这些问题在第5版的ES规范都已经修正了,不管是基于字面量的还是构造器的,正则都是创建新对象。

 

关联数组

从上面代码我们知道,原型继承的方法是:假如A需要继承于B,那么A.prototype(A的原型) = new B()(作为B的实列) 即可实现A继承于B; 因此我们下面可以初始化一个空的构造函数;然后把对象赋值给构造函数的原型,然后返回该构造函数的实列; 即可实现继承; 如下代码:

各种文字静态讨论,JavaScript对象(经常是用对象初始化器{}来创建)被称为哈希表哈希表或其它简单的称谓:哈希(Ruby或Perl里的概念), 管理数组(PHP里的概念),词典 (Python里的概念)等。

 

只有这样的术语,主要是因为他们的结构都是相似的,就是使用“键-值”对来存储对象,完全符合“关联数组 ”或“哈希表 ”理论定义的数据结构。 此外,哈希表抽象数据类型通常是在实现层面使用。

if(typeof Object.create !== 'function') {

Object.create = function(o) {

var F = new Function();

F.prototype = o;

return new F();

}

}

var a = {

name: 'longen',

getName: function(){

return this.name;

}

};

var b = {};

b = Object.create(a);

console.log(typeof b); //object

console.log(b.name); // longen

console.log(b.getName()); // longen

但是,尽管术语上来描述这个概念,但实际上这个是错误,从ECMAScript来看:ECMAScript只有一个对象以及类型以及它的子类型,这和“键-值”对存储没有什么区别,因此在这上面没有特别的概念。 因为任何对象的内部属性都可以存储为键-值”对:

 

复制代码 代码如下:

如上代码:我们先检测Object是否已经有Object.create该方法;如果没有的话就创建一个; 该方法内创建一个空的构造器,把参数对象传递给构造函数的原型,最后返回该构造函数的实列,就实现了继承方式;如上测试代码:先定义一个a对象,有成员属性name=’longen’,还有一个getName()方法;最后返回该name属性; 然后定义一个b空对象,使用Object.create(a);把a对象继承给b对象,因此b对象也有属性name和成员方法getName();

var a = {x: 10};
a['y'] = 20;
a.z = 30;
 
var b = new Number(1);
b.x = 10;
b.y = 20;
b['z'] = 30;
 
var c = new Function('');
c.x = 10;
c.y = 20;
c['z'] = 30;
 
// 等等,任意对象的子类型"subtype"

 

此外,由于在ECMAScript中对象可以是空的,所以"hash"的概念在这里也是不正确的:

理解原型查找原理:对象查找先在该构造函数内查找对应的属性,如果该对象没有该属性的话,

复制代码 代码如下:

 

Object.prototype.x = 10;
 
var a = {}; // 创建空"hash"
 
alert(a["x"]); // 10, 但不为空
alert(a.toString); // function
 
a["y"] = 20; // 添加新的键值对到 "hash"
alert(a["y"]); // 20
 
Object.prototype.y = 20; // 添加原型属性
 
delete a["y"]; // 删除
alert(a["y"]); // 但这里key和value依然有值 – 20

那么javascript会试着从该原型上去查找,如果原型对象中也没有该属性的话,那么它们会从原型中的原型去查找,直到查找的Object.prototype也没有该属性的话,那么就会返回undefined;因此我们想要仅在该对象内查找的话,为了提高性能,我们可以使用hasOwnProperty()来判断该对象内有没有该属性,如果有的话,就执行代码(使用for-in循环查找):如下:

请注意, ES5标准可以让我们创建没原型的对象(使用Object.create(null)方法实现)对,从这个角度来说,这样的对象可以称之为哈希表:

 

复制代码 代码如下:

var obj = {

"name":'tugenhua',

"age":'28'

};

// 使用for-in循环

for(var i in obj) {

if(obj.hasOwnProperty(i)) {

console.log(obj[i]); //tugenhua 28

}

}

var aHashTable = Object.create(null);
console.log(aHashTable.toString); // 未定义

 

此外,一些属性有特定的getter / setter方法​​,所以也可能导致混淆这个概念:

如上使用for-in循环查找对象里面的属性,但是我们需要明白的是:for-in循环查找对象的属性,它是不保证顺序的,for-in循环和for循环;最本质的区别是:for循环是有顺序的,for-in循环遍历对象是无序的,因此我们如果需要对象保证顺序的话,可以把对象转换为数组来,然后再使用for循环遍历即可;

复制代码 代码如下:

 

var a = new String("foo");
a['length'] = 10;
alert(a['length']); // 3

下面我们来谈谈原型继承的优点和缺点

然而,即使认为“哈希”可能有一个“原型”(例如,在Ruby或Python里委托哈希对象的类),在ECMAScript里,这个术语也是不对的,因为2个表示法之间没有语义上的区别(即用点表示法a.b和a["b"]表示法)。

 

在ECMAScript中的“property属性”的概念语义上和"key"、数组索引、方法没有分开的,这里所有对象的属性读写都要遵循统一的规则:检查原型链。

// 先看下面的代码:

// 定义构造函数A,定义特权属性和特权方法

function A(x) {

this.x1 = x;

this.getX1 = function(){

return this.x1;

}

}

// 定义构造函数B,定义特权属性和特权方法

function B(x) {

this.x2 = x;

this.getX2 = function(){

return this.x1 this.x2;

}

}

B.prototype = new A(1);

在下面Ruby的例子中,我们可以看到语义上的区别:

 

复制代码 代码如下:

B.prototype = new A(1);这句代码执行的时候,B的原型继承于A,因此B.prototype也有A的属性和方法,即:B.prototype.x1 = 1; B.prototype.getX1 方法;但是B也有自己的特权属性x2和特权方法getX2; 如下代码:

a = {}
a.class # Hash
 
a.length # 0
 
# new "key-value" pair
a['length'] = 10;
 
# 语义上,用点访问的是属性或方法,而不是key
 
a.length # 1
 
# 而索引器访问访问的是hash里的key
 
a['length'] # 10
 
# 就类似于在现有对象上动态声明Hash类
# 然后声明新属性或方法
 
class Hash
  def z
    100
  end
end
 
# 新属性可以访问
 
a.z # 100
 
# 但不是"key"
 
a['z'] # nil

 

ECMA-262-3标准并没有定义“哈希”(以及类似)的概念。但是,有这样的结构理论的话,那可能以此命名的对象。

function C(x) {

this.x3 = x;

this.getX3 = function(){

return this.x3 this.x2;

}

}

C.prototype = new B(2);

C.prototype = new B(2);这句代码执行的时候,C的原型继承于B,因此C.prototype.x2 = 2; C.prototype.getX2方法且C也有自己的特权属性x3和特权方法getX3,

var b = new B(2);

var c = new C(3);

console.log(b.x1); // 1

console.log(c.x1); // 1

console.log(c.getX3()); // 5

console.log(c.getX2()); // 3

var b = new B(2);

对象转换

 

将对象转化成原始值可以用valueOf方法,正如我们所说的,当函数的构造函数调用做为function(对于某些类型的),但如果不用new关键字就是将对象转化成原始值,就相当于隐式的valueOf方法调用:

实列化B的时候 b.x1 首先会在构造函数内查找x1属性,没有找到,由于B的原型继承于A,因此A有x1属性,因此B.prototype.x1 = 1找到了;var c = new C(3); 实列化C的时候,从上面的代码可以看到C继承于B,B继承于A,因此在C函数中没有找到x1属性,会往原型继续查找,直到找到父元素A有x1属性,因此c.x1 = 1;c.getX3()方法; 返回this.x3 this.x2 this.x3 = 3;this.x2 是B的属性,因此this.x2 = 2;c.getX2(); 查找的方法也一样,不再解释

复制代码 代码如下:

 

var a = new Number(1);
var primitiveA = Number(a); // 隐式"valueOf"调用
var alsoPrimitiveA = a.valueOf(); // 显式调用
 
alert([
  typeof a, // "object"
  typeof primitiveA, // "number"
  typeof alsoPrimitiveA // "number"
]);

prototype的缺点与优点如下:

这种方式允许对象参与各种操作,例如:

 

复制代码 代码如下:

优点是:能够允许多个对象实列共享原型对象的成员及方法,

var a = new Number(1);
var b = new Number(2);
 
alert(a b); // 3
 
// 甚至
 
var c = {
  x: 10,
  y: 20,
  valueOf: function () {
    return this.x this.y;
  }
};
 
var d = {
  x: 30,
  y: 40,
  // 和c的valueOf功能一样
  valueOf: c.valueOf
};
 
alert(c d); // 100

 

valueOf的默认值会根据根据对象的类型改变(如果不被覆盖的话),对某些对象,他返回的是this——例如:Object.prototype.valueOf(),还有计算型的值:Date.prototype.valueOf()返回的是日期时间:

缺点是:1. 每个构造函数只有一个原型,因此不直接支持多重继承;

复制代码 代码如下:

 

var a = {};
alert(a.valueOf() === a); // true, "valueOf"返回this
 
var d = new Date();
alert(d.valueOf()); // time
alert(d.valueOf() === d.getTime()); // true

2. 不能很好地支持多参数或动态参数的父类。在原型继承阶段,用户还不能决定以

此外,对象还有一个更原始的代表性——字符串展示。 这个toString方法是可靠的,它在某些操作上是自动使用的:

 

复制代码 代码如下:

什么参数来实列化构造函数。

var a = {
  valueOf: function () {
    return 100;
  },
  toString: function () {
    return '__test';
  }
};
 
// 这个操作里,toString方法自动调用
alert(a); // "__test"
 
// 但是这里,调用的却是valueOf()方法
alert(a 10); // 110
 
// 但,一旦valueOf删除以后
// toString又可以自动调用了
delete a.valueOf;
alert(a 10); // "_test10"

 

Object.prototype上定义的toString方法具有特殊意义,它返回的我们下面将要讨论的内部[[Class]]属性值。

四:理解使用类继承(继承的更好的方案)

和转化成原始值(ToPrimitive)相比,将值转化成对象类型也有一个转化规范(ToObject)。

 

一个显式方法是使用内置的Object构造函数作为function来调用ToObject(有些类似通过new关键字也可以):

类继承也叫做构造函数继承,在子类中执行父类的构造函数;实现原理是:可以将一个构造函数A的方法赋值给另一个构造函数B,然后调用该方法,使构造函数A在构造函数B内部被执行,这时候构造函数B就拥有了构造函数A中的属性和方法,这就是使用类继承实现B继承与A的基本原理;

复制代码 代码如下:

 

var n = Object(1); // [object Number]
var s = Object('test'); // [object String]
 
// 一些类似,使用new操作符也可以
var b = new Object(true); // [object Boolean]
 
// 应用参数new Object的话创建的是简单对象
var o = new Object(); // [object Object]
 
// 如果参数是一个现有的对象
// 那创建的结果就是简单返回该对象
var a = [];
alert(a === new Object(a)); // true
alert(a === Object(a)); // true

如下代码实现demo:

关于调用内置构造函数,使用还是不适用new操作符没有通用规则,取决于构造函数。 例如Array或Function当使用new操作符的构造函数或者不使用new操作符的简单函数使用产生相同的结果的:

 

复制代码 代码如下:

function A(x) {

this.x = x;

this.say = function(){

return this.x;

}

}

function B(x,y) {

this.m = A; // 把构造函数A作为一个普通函数引用给临时方法m

this.m(x); // 执行构造函数A;

delete this.m; // 清除临时方法this.m

this.y = y;

this.method = function(){

return this.y;

}

}

var a = new A(1);

var b = new B(2,3);

console.log(a.say()); //输出1, 执行构造函数A中的say方法

console.log(b.say()); //输出2, 能执行该方法说明被继承了A中的方法

console.log(b.method()); // 输出3, 构造函数也拥有自己的方法

var a = Array(1, 2, 3); // [object Array]
var b = new Array(1, 2, 3); // [object Array]
var c = [1, 2, 3]; // [object Array]
 
var d = Function(''); // [object Function]
var e = new Function(''); // [object Function]

 

有些操作符使用的时候,也有一些显示和隐式转化:

上面的代码实现了简单的类继承的基础,但是在复杂的编程中是不会使用上面的方法的,因为上面的代码不够严谨;代码的耦合性高;我们可以使用更好的方法如下:

复制代码 代码如下:

 

var a = 1;
var b = 2;
 
// 隐式
var c = a b; // 3, number
var d = a b '5' // "35", string
 
// 显式
var e = '10'; // "10", string
var f = e; // 10, number
var g = parseInt(e, 10); // 10, number
 
// 等等

function A(x) {

this.x = x;

}

A.prototype.getX = function(){

return this.x;

}

// 实例化A

var a = new A(1);

console.log(a.x); // 1

console.log(a.getX()); // 输出1

// 现在我们来创建构造函数B,让其B继承与A,如下代码:

function B(x,y) {

this.y = y;

A.call(this,x);

}

B.prototype = new A(); // 原型继承

console.log(B.prototype.constructor); // 输出构造函数A,指针指向与构造函数A

B.prototype.constructor = B; // 重新设置构造函数,使之指向B

console.log(B.prototype.constructor); // 指向构造函数B

B.prototype.getY = function(){

return this.y;

}

var b = new B(1,2);

console.log(b.x); // 1

console.log(b.getX()); // 1

console.log(b.getY()); // 2

 

// 下面是演示对构造函数getX进行重写的方法如下:

B.prototype.getX = function(){

return this.x;

}

var b2 = new B(10,20);

console.log(b2.getX()); // 输出10

属性的特性

 

所有的属性(property) 都可以有很多特性(attributes)。

下面我们来分析上面的代码:

1.{ReadOnly}——忽略向属性赋值的写操作尝,但只读属性可以由宿主环境行为改变——也就是说不是“恒定值” ;
2.{DontEnum}——属性不能被for..in循环枚举
3.{DontDelete}——糊了delete操作符的行为被忽略(即删不掉);
4.{Internal}——内部属性,没有名字(仅在实现层面使用),ECMAScript里无法访问这样的属性。

 

注意,在ES5里{ReadOnly},{DontEnum}和{DontDelete}被重新命名为[[Writable]],[[Enumerable]]和[[Configurable]],可以手工通过Object.defineProperty或类似的方法来管理这些属性。

在构造函数B内,使用A.call(this,x);这句代码的含义是:我们都知道使用call或者apply方法可以改变this指针指向,从而可以实现类的继承,因此在B构造函数内,把x的参数传递给A构造函数,并且继承于构造函数A中的属性和方法;

复制代码 代码如下:

 

var foo = {};
 
Object.defineProperty(foo, "x", {
  value: 10,
  writable: true, // 即{ReadOnly} = false
  enumerable: false, // 即{DontEnum} = true
  configurable: true // 即{DontDelete} = false
});
 
console.log(foo.x); // 10
 
// 通过descriptor获取特性集attributes
var desc = Object.getOwnPropertyDescriptor(foo, "x");
 
console.log(desc.enumerable); // false
console.log(desc.writable); // true
// 等等

使用这句代码:B.prototype = new A(); 可以实现原型继承,也就是B可以继承A中的原型所有的方法;console.log(B.prototype.constructor); 打印出输出构造函数A,指针指向与构造函数A;我们明白的是,当定义构造函数时候,其原型对象默认是一个Object类型的一个实例,其构造器默认会被设置为构造函数本身,如果改动构造函数prototype属性值,使其指向于另一个对象的话,那么新对象就不会拥有原来的constructor的值,比如第一次打印console.log(B.prototype.constructor); 指向于被实例化后的构造函数A,重写设置B的constructor的属性值的时候,第二次打印就指向于本身B;因此B继承与构造A及其原型的所有属性和方法,当然我们也可以对构造函数B重写构造函数A中的方法,如上面最后几句代码是对构造函数A中的getX方法进行重写,来实现自己的业务~;

内部属性和方法

 

对象也可以有内部属性(实现层面的一部分),并且ECMAScript程序无法直接访问(但是下面我们将看到,一些实现允许访问一些这样的属性)。 这些属性通过嵌套的中括号[[ ]]进行访问。我们来看其中的一些,这些属性的描述可以到规范里查阅到。

五:建议使用封装类实现继承

每个对象都应该实现如下内部属性和方法:

 

1.[[Prototype]]——对象的原型(将在下面详细介绍)
2.[[Class]]——字符串对象的一种表示(例如,Object Array ,Function Object,Function等);用来区分对象
3.[[Get]]——获得属性值的方法
4.[[Put]]——设置属性值的方法
5.[[CanPut]]——检查属性是否可写
6.[[HasProperty]]——检查对象是否已经拥有该属性
7.[[Delete]]——从对象删除该属性
8.[[DefaultValue]]返回对象对于的原始值(调用valueOf方法,某些对象可能会抛出TypeError异常)。
通过Object.prototype.toString()方法可以间接得到内部属性[[Class]]的值,该方法应该返回下列字符串: "[object " [[Class]] "]" 。例如:

封装类实现继承的基本原理:先定义一个封装函数extend;该函数有2个参数,Sub代表子类,Sup代表超类;在函数内,先定义一个空函数F, 用来实现功能中转,先设置F的原型为超类的原型,然后把空函数的实例传递给子类的原型,使用一个空函数的好处是:避免直接实例化超类可能会带来系统性能问题,比如超类的实例很大的话,实例化会占用很多内存;

复制代码 代码如下:

 

var getClass = Object.prototype.toString;
 
getClass.call({}); // [object Object]
getClass.call([]); // [object Array]
getClass.call(new Number(1)); // [object Number]
// 等等

如下代码:

这个功能通常是用来检查对象用的,但规范上说宿主对象的[[Class]]可以为任意值,包括内置对象的[[Class]]属性的值,所以理论上来看是不能100%来保证准确的。例如,document.childNodes.item(...)方法的[[Class]]属性,在IE里返回"String",但其它实现里返回的确实"Function"。

 

复制代码 代码如下:

function extend(Sub,Sup) {

//Sub表示子类,Sup表示超类

// 首先定义一个空函数

var F = function(){};

 

// 设置空函数的原型为超类的原型

F.prototype = Sup.prototype;

 

// 实例化空函数,并把超类原型引用传递给子类

Sub.prototype = new F();

 

// 重置子类原型的构造器为子类自身

Sub.prototype.constructor = Sub;

 

// 在子类中保存超类的原型,避免子类与超类耦合

Sub.sup = Sup.prototype;

 

if(Sup.prototype.constructor === Object.prototype.constructor) {

// 检测超类原型的构造器是否为原型自身

Sup.prototype.constructor = Sup;

}

 

}

测试代码如下:

// 下面我们定义2个类A和类B,我们目的是实现B继承于A

function A(x) {

this.x = x;

this.getX = function(){

return this.x;

}

}

A.prototype.add = function(){

return this.x this.x;

}

A.prototype.mul = function(){

return this.x * this.x;

}

// 构造函数B

function B(x){

A.call(this,x); // 继承构造函数A中的所有属性及方法

}

extend(B,A); // B继承于A

var b = new B(11);

console.log(b.getX()); // 11

console.log(b.add()); // 22

console.log(b.mul()); // 121

// in IE - "String", in other - "Function"
alert(getClass.call(document.childNodes.item));

 

构造函数

注意:在封装函数中,有这么一句代码:Sub.sup = Sup.prototype; 我们现在可以来理解下它的含义:

因此,正如我们上面提到的,在ECMAScript中的对象是通过所谓的构造函数来创建的。

 

Constructor is a function that creates and initializes the newly created object.
构造函数是一个函数,用来创建并初始化新创建的对象。
对象创建(内存分配)是由构造函数的内部方法[[Construct]]负责的。该内部方法的行为是定义好的,所有的构造函数都是使用该方法来为新对象分配内存的。

比如在B继承与A后,我给B函数的原型再定义一个与A相同的原型相同的方法add();

而初始化是通过新建对象上下上调用该函数来管理的,这是由构造函数的内部方法[[Call]]来负责任的。

 

注意,用户代码只能在初始化阶段访问,虽然在初始化阶段我们可以返回不同的对象(忽略第一阶段创建的tihs对象):

如下代码

复制代码 代码如下:

 

function A() {
  // 更新新创建的对象
  this.x = 10;
  // 但返回的是不同的对象
  return [1, 2, 3];
}
 
var a = new A();
console.log(a.x, a); undefined, [1, 2, 3]

extend(B,A); // B继承于A

var b = new B(11);

B.prototype.add = function(){

return this.x "" this.x;

}

console.log(b.add()); // 1111

引用15章函数——创建函数的算法小节,我们可以看到该函数是一个原生对象,包含[[Construct]] ]和[[Call]] ]属性以及显示的prototype原型属性——未来对象的原型(注:NativeObject是对于native object原生对象的约定,在下面的伪代码中使用)。

 

复制代码 代码如下:

那么B函数中的add方法会覆盖A函数中的add方法;因此为了不覆盖A类中的add()方法,且调用A函数中的add方法;可以如下编写代码:

F = new NativeObject();
 
F.[[Class]] = "Function"
 
.... // 其它属性
 
F.[[Call]] = <reference to function> // function自身
 
F.[[Construct]] = internalConstructor // 普通的内部构造函数
 
.... // 其它属性
 
// F构造函数创建的对象原型
__objectPrototype = {};
__objectPrototype.constructor = F // {DontEnum}
F.prototype = __objectPrototype

 

[[Call]] ]是除[[Class]]属性(这里等同于"Function" )之外区分对象的主要方式,因此,对象的内部[[Call]]属性作为函数调用。 这样的对象用typeof运算操作符的话返回的是"function"。然而它主要是和原生对象有关,有些情况的实现在用typeof获取值的是不一样的,例如:window.alert (...)在IE中的效果:

B.prototype.add = function(){

//return this.x "" this.x;

return B.sup.add.call(this);

}

console.log(b.add()); // 22

复制代码 代码如下:

 

// IE浏览器中 - "Object", "object", 其它浏览器 - "Function", "function"
alert(Object.prototype.toString.call(window.alert));
alert(typeof window.alert); // "Object"

B.sup.add.call(this); 中的B.sup就包含了构造函数A函数的指针,因此包含A函数的所有属性和方法;因此可以调用A函数中的add方法;

内部方法[[Construct]]是通过使用带new运算符的构造函数来激活的,正如我们所说的这个方法是负责内存分配和对象创建的。如果没有参数,调用构造函数的括号也可以省略:

 

复制代码 代码如下:

如上是实现继承的几种方式,类继承和原型继承,但是这些继承无法继承DOM对象,也不支持继承系统静态对象,静态方法等;比如Date对象如下:

function A(x) { // constructor А
  this.x = x || 10;
}
 
// 不传参数的话,括号也可以省略
var a = new A; // or new A();
alert(a.x); // 10
 
// 显式传入参数x
var b = new A(20);
alert(b.x); // 20

 

我们也知道,构造函数(初始化阶段)里的shis被设置为新创建的对象 。

// 使用类继承Date对象

function D(){

Date.apply(this,arguments); // 调用Date对象,对其引用,实现继承

}

var d = new D();

console.log(d.toLocaleString()); // [object object]

让我们研究一下对象创建的算法。

 

对象创建的算法

如上代码运行打印出object,我们可以看到使用类继承无法实现系统静态方法date对象的继承,因为他不是简单的函数结构,对声明,赋值和初始化都进行了封装,因此无法继承;

内部方法[[Construct]] 的行为可以描述成如下:

 

复制代码 代码如下:

下面我们再来看看使用原型继承date对象;

F.[[Construct]](initialParameters):
 
O = new NativeObject();
 
// 属性[[Class]]被设置为"Object"
O.[[Class]] = "Object"
 
// 引用F.prototype的时候获取该对象g
var __objectPrototype = F.prototype;
 
// 如果__objectPrototype是对象,就:
O.[[Prototype]] = __objectPrototype
// 否则:
O.[[Prototype]] = Object.prototype;
// 这里O.[[Prototype]]是Object对象的原型
 
// 新创建对象初始化的时候应用了F.[[Call]]
// 将this设置为新创建的对象O
// 参数和F里的initialParameters是一样的
R = F.[[Call]](initialParameters); this === O;
// 这里R是[[Call]]的返回值
// 在JS里看,像这样:
// R = F.apply(O, initialParameters);
 
// 如果R是对象
return R
// 否则
return O

 

请注意两个主要特点:

function D(){}

D.prototype = new D();

var d = new D();

console.log(d.toLocaleString());//[object object]

1.首先,新创建对象的原型是从当前时刻函数的prototype属性获取的(这意味着同一个构造函数创建的两个创建对象的原型可以不同是因为函数的prototype属性也可以不同)。
2.其次,正如我们上面提到的,如果在对象初始化的时候,[[Call]]返回的是对象,这恰恰是用于整个new操作符的结果:

 

复制代码 代码如下:

我们从代码中看到,使用原型继承也无法继承Date静态方法;但是我们可以如下封装代码继承:

function A() {}
A.prototype.x = 10;
 
var a = new A();
alert(a.x); // 10 – 从原型上得到
 
// 设置.prototype属性为新对象
// 为什么显式声明.constructor属性将在下面说明
A.prototype = {
  constructor: A,
  y: 100
};
 
var b = new A();
// 对象"b"有了新属性
alert(b.x); // undefined
alert(b.y); // 100 – 从原型上得到
 
// 但a对象的原型依然可以得到原来的结果
alert(a.x); // 10 - 从原型上得到
 
function B() {
  this.x = 10;
  return new Array();
}
 
// 如果"B"构造函数没有返回(或返回this)
// 那么this对象就可以使用,但是下面的情况返回的是array
var b = new B();
alert(b.x); // undefined
alert(Object.prototype.toString.call(b)); // [object Array]

 

让我们来详细了解一下原型

function D(){

var d = new Date(); // 实例化Date对象

d.get = function(){ // 定义本地方法,间接调用Date对象的方法

console.log(d.toLocaleString());

}

return d;

}

var d = new D();

d.get(); // 2015/12/21 上午12:08:38

原型

 

每个对象都有一个原型(一些系统对象除外)。原型通信是通过内部的、隐式的、不可直接访问[[Prototype]]原型属性来进行的,原型可以是一个对象,也可以是null值。

六:理解使用复制继承

属性构造函数(Property constructor)

 

上面的例子有有2个重要的知识点,第一个是关于函数的constructor属性的prototype属性,在函数创建的算法里,我们知道constructor属性在函数创建阶段被设置为函数的prototype属性,constructor属性的值是函数自身的重要引用:

复制继承的基本原理是:先设计一个空对象,然后使用for-in循环来遍历对象的成员,将该对象的成员一个一个复制给新的空对象里面;这样就实现了复制继承了;如下代码:

复制代码 代码如下:

 

function A() {}
var a = new A();
alert(a.constructor); // function A() {}, by delegation
alert(a.constructor === A); // true

function A(x,y) {

this.x = x;

this.y = y;

this.add = function(){

return this.x this.y;

}

}

A.prototype.mul = function(){

return this.x * this.y;

}

var a = new A(2,3);

var obj = {};

for(var i in a) {

obj[i] = a[i];

}

console.log(obj); // object

console.log(obj.x); // 2

console.log(obj.y); // 3

console.log(obj.add()); // 5

console.log(obj.mul()); // 6

通常在这种情况下,存在着一个误区:constructor构造属性作为新创建对象自身的属性是错误的,但是,正如我们所看到的的,这个属性属于原型并且通过继承来访问对象。

 

通过继承constructor属性的实例,可以间接得到的原型对象的引用:

如上代码:先定义一个构造函数A,函数里面有2个属性x,y,还有一个add方法,该构造函数原型有一个mul方法,首先实列化下A后,再创建一个空对象obj,遍历对象一个个复制给空对象obj,从上面的打印效果来看,我们可以看到已经实现了复制继承了;对于复制继承,我们可以封装成如下方法来调用:

复制代码 代码如下:

 

function A() {}
A.prototype.x = new Number(10);
 
var a = new A();
alert(a.constructor.prototype); // [object Object]
 
alert(a.x); // 10, 通过原型
// 和a.[[Prototype]].x效果一样
alert(a.constructor.prototype.x); // 10
 
alert(a.constructor.prototype.x === a.x); // true

// 为Function扩展复制继承方法

Function.prototype.extend = function(o) {

for(var i in o) {

//把参数对象的成员复制给当前对象的构造函数原型对象

this.constructor.prototype[i] = o[i];

}

}

// 测试代码如下:

var o = function(){};

o.extend(new A(1,2));

console.log(o.x); // 1

console.log(o.y); // 2

console.log(o.add()); // 3

console.log(o.mul()); // 2

但请注意,函数的constructor和prototype属性在对象创建以后都可以重新定义的。在这种情况下,对象失去上面所说的机制。如果通过函数的prototype属性去编辑元素的prototype原型的话(添加新对象或修改现有对象),实例上将看到新添加的属性。

 

然而,如果我们彻底改变函数的prototype属性(通过分配一个新的对象),那原始构造函数的引用就是丢失,这是因为我们创建的对象不包括constructor属性:

上面封装的扩展继承方法中的this对象指向于当前实列化后的对象,而不是指向于构造函数本身,因此要使用原型扩展成员的话,就需要使用constructor属性来指向它的构造器,然后通过prototype属性指向构造函数的原型;

复制代码 代码如下:

 

function A() {}
A.prototype = {
  x: 10
};
 
var a = new A();
alert(a.x); // 10
alert(a.constructor === A); // false!

复制继承有如下优点:

因此,对函数的原型引用需要手工恢复:

 

复制代码 代码如下:

  1. 它不能继承系统核心对象的只读方法和属性

function A() {}
A.prototype = {
  constructor: A,
  x: 10
};
 
var a = new A();
alert(a.x); // 10
alert(a.constructor === A); // true

 

注意虽然手动恢复了constructor属性,和原来丢失的原型相比,{DontEnum}特性没有了,也就是说A.prototype里的for..in循环语句不支持了,不过第5版规范里,通过[[Enumerable]] 特性提供了控制可枚举状态enumerable的能力。

  1. 如果对象数据非常多的话,这样一个个复制的话,性能是非常低的;

复制代码 代码如下:

 

var foo = {x: 10};
 
Object.defineProperty(foo, "y", {
  value: 20,
  enumerable: false // aka {DontEnum} = true
});
 
console.log(foo.x, foo.y); // 10, 20
 
for (var k in foo) {
  console.log(k); // only "x"
}
 
var xDesc = Object.getOwnPropertyDescriptor(foo, "x");
var yDesc = Object.getOwnPropertyDescriptor(foo, "y");
 
console.log(
  xDesc.enumerable, // true
  yDesc.enumerable  // false
);

  1. 只有对象被实列化后,才能给遍历对象的成员和属性,相对来说不够灵活;

显式prototype和隐式[[Prototype]]属性

深远理解,浓密理解Javascript面向对象编制程序。 

通常,一个对象的原型通过函数的prototype属性显式引用是不正确的,他引用的是同一个对象,对象的[[Prototype]]属性:

4. 复制继承只是简单的赋值,所以如果赋值的对象是引用类型的对象的话,可能会存在一些副作用;如上我们看到有如上一些缺点,下面我们可以使用clone(克隆的方式)来优化下:

a.[[Prototype]] ----> Prototype <---- A.prototype

 

此外, 实例的[[Prototype]]值确实是在构造函数的prototype属性上获取的。

基本思路是:为Function扩展一个方法,该方法能够把参数对象赋值赋值一个空构造函数的原型对象,然后实列化构造函数并返回实列对象,这样该对象就拥有了该对象的所有成员;代码如下:

然而,提交prototype属性不会影响已经创建对象的原型(只有在构造函数的prototype属性改变的时候才会影响到),就是说新创建的对象才有有新的原型,而已创建对象还是引用到原来的旧原型(这个原型已经不能被再被修改了)。

 

复制代码 代码如下:

Function.prototype.clone = function(o){

function Temp(){};

Temp.prototype = o;

return Temp();

}

// 测试代码如下:

Function.clone(new A(1,2));

console.log(o.x); // 1

console.log(o.y); // 2

console.log(o.add()); // 3

console.log(o.mul()); // 2

// 在修改A.prototype原型之前的情况
a.[[Prototype]] ----> Prototype <---- A.prototype
 
// 修改之后
A.prototype ----> New prototype // 新对象会拥有这个原型
a.[[Prototype]] ----> Prototype // 引导的原来的原型上

例如:

复制代码 代码如下:

function A() {}
A.prototype.x = 10;
 
var a = new A();
alert(a.x); // 10
 
A.prototype = {
  constructor: A,
  x: 20
  y: 30
};
 
// 对象a是通过隐式的[[Prototype]]引用从原油的prototype上获取的值
alert(a.x); // 10
alert(a.y) // undefined
 
var b = new A();
 
// 但新对象是从新原型上获取的值
alert(b.x); // 20
alert(b.y) // 30

因此,有的文章说“动态修改原型将影响所有的对象都会拥有新的原型”是错误的,新原型仅仅在原型修改以后的新创建对象上生效。

这里的主要规则是:对象的原型是对象的创建的时候创建的,并且在此之后不能修改为新的对象,如果依然引用到同一个对象,可以通过构造函数的显式prototype引用,对象创建以后,只能对原型的属性进行添加或修改。

非标准的__proto__属性

然而,有些实现(例如SpiderMonkey),提供了不标准的__proto__显式属性来引用对象的原型:

复制代码 代码如下:

function A() {}
A.prototype.x = 10;
 
var a = new A();
alert(a.x); // 10
 
var __newPrototype = {
  constructor: A,
  x: 20,
  y: 30
};
 
// 引用到新对象
A.prototype = __newPrototype;
 
var b = new A();
alert(b.x); // 20
alert(b.y); // 30
 
// "a"对象使用的依然是旧的原型
alert(a.x); // 10
alert(a.y); // undefined
 
// 显式修改原型
a.__proto__ = __newPrototype;
 
// 现在"а"对象引用的是新对象
alert(a.x); // 20
alert(a.y); // 30

注意,ES5提供了Object.getPrototypeOf(O)方法,该方法直接返回对象的[[Prototype]]属性——实例的初始原型。 然而,和__proto__相比,它只是getter,它不允许set值。

复制代码 代码如下:

var foo = {};
Object.getPrototypeOf(foo) == Object.prototype; // true

对象独立于构造函数 因为实例的原型独立于构造函数和构造函数的prototype属性,构造函数完成了自己的主要工作(创建对象)以后可以删除。原型对象通过引用[[Prototype]]属性继续存在:

复制代码 代码如下:

function A() {}
A.prototype.x = 10;
 
var a = new A();
alert(a.x); // 10
 
// 设置A为null - 显示引用构造函数
A = null;
 
// 但如果.constructor属性没有改变的话,
// 依然可以通过它创建对象
var b = new a.constructor();
alert(b.x); // 10
 
// 隐式的引用也删除掉
delete a.constructor.prototype.constructor;
delete b.constructor.prototype.constructor;
 
// 通过A的构造函数再也不能创建对象了
// 但这2个对象依然有自己的原型
alert(a.x); // 10
alert(b.x); // 10

instanceof操作符的特性 我们是通过构造函数的prototype属性来显示引用原型的,这和instanceof操作符有关。该操作符是和原型链一起工作的,而不是构造函数,考虑到这一点,当检测对象的时候往往会有误解:

复制代码 代码如下:

if (foo instanceof Foo) {
  ...
}

这不是用来检测对象foo是否是用Foo构造函数创建的,所有instanceof运算符只需要一个对象属性——foo.[[Prototype]],在原型链中从Foo.prototype开始检查其是否存在。instanceof运算符是通过构造函数里的内部方法[[HasInstance]]来激活的。

让我们来看看这个例子:

复制代码 代码如下:

function A() {}
A.prototype.x = 10;
 
var a = new A();
alert(a.x); // 10
 
alert(a instanceof A); // true
 
// 如果设置原型为null
A.prototype = null;
 
// ..."a"依然可以通过a.[[Prototype]]访问原型
alert(a.x); // 10
 
// 不过,instanceof操作符不能再正常使用了
// 因为它是从构造函数的prototype属性来实现的
alert(a instanceof A); // 错误,A.prototype不是对象

另一方面,可以由构造函数来创建对象,但如果对象的[[Prototype]]属性和构造函数的prototype属性的值设置的是一样的话,instanceof检查的时候会返回true:

复制代码 代码如下:

function B() {}
var b = new B();
 
alert(b instanceof B); // true
 
function C() {}
 
var __proto = {
  constructor: C
};
 
C.prototype = __proto;
b.__proto__ = __proto;
 
alert(b instanceof C); // true
alert(b instanceof B); // false

原型可以存放方法并共享属性 大部分程序里使用原型是用来存储对象的方法、默认状态和共享对象的属性。

事实上,对象可以拥有自己的状态 ,但方法通常是一样的。 因此,为了内存优化,方法通常是在原型里定义的。 这意味着,这个构造函数创建的所有实例都可以共享找个方法。

复制代码 代码如下:

function A(x) {
  this.x = x || 100;
}
 
A.prototype = (function () {
 
  // 初始化上下文
  // 使用额外的对象
 
  var _someSharedVar = 500;
 
  function _someHelper() {
    alert('internal helper: ' _someSharedVar);
  }
 
  function method1() {
    alert('method1: ' this.x);
  }
 
  function method2() {
    alert('method2: ' this.x);
    _someHelper();
  }
 
  // 原型自身
  return {
    constructor: A,
    method1: method1,
    method2: method2
  };
 
})();
 
var a = new A(10);
var b = new A(20);
 
a.method1(); // method1: 10
a.method2(); // method2: 10, internal helper: 500
 
b.method1(); // method1: 20
b.method2(); // method2: 20, internal helper: 500
 
// 2个对象使用的是原型里相同的方法
alert(a.method1 === b.method1); // true
alert(a.method2 === b.method2); // true

读写属性

正如我们提到,读取和写入属性值是通过内部的[[Get]]和[[Put]]方法。这些内部方法是通过属性访问器激活的:点标记法或者索引标记法:

复制代码 代码如下:

// 写入
foo.bar = 10; // 调用了[[Put]]
 
console.log(foo.bar); // 10, 调用了[[Get]]
console.log(foo['bar']); // 效果一样

让我们用伪代码来看一下这些方法是如何工作的:

[[Get]]方法

[[Get]]也会从原型链中查询属性,所以通过对象也可以访问原型中的属性。

O.[[Get]](P):

复制代码 代码如下:

// 如果是自己的属性,就返回
if (O.hasOwnProperty(P)) {
  return O.P;
}
 
// 否则,继续分析原型
var __proto = O.[[Prototype]];
 
// 如果原型是null,返回undefined
// 这是可能的:最顶层Object.prototype.[[Prototype]]是null
if (__proto === null) {
  return undefined;
}
 
// 否则,对原型链递归调用[[Get]],在各层的原型中查找属性
// 直到原型为null
return __proto.[[Get]](P)

请注意,因为[[Get]]在如下情况也会返回undefined:

复制代码 代码如下:

if (window.someObject) {
  ...
}

这里,在window里没有找到someObject属性,然后会在原型里找,原型的原型里找,以此类推,如果都找不到,按照定义就返回undefined。

注意:in操作符也可以负责查找属性(也会查找原型链):

复制代码 代码如下:

if ('someObject' in window) {
  ...
}

这有助于避免一些特殊问题:比如即便someObject存在,在someObject等于false的时候,第一轮检测就通不过。

[[Put]]方法

[[Put]]方法可以创建、更新对象自身的属性,并且掩盖原型里的同名属性。

O.[[Put]](P, V):

复制代码 代码如下:

// 如果不能给属性写值,就退出
if (!O.[[CanPut]](P)) {
  return;
}
 
// 如果对象没有自身的属性,就创建它
// 所有的attributes特性都是false
if (!O.hasOwnProperty(P)) {
  createNewProperty(O, P, attributes: {
    ReadOnly: false,
    DontEnum: false,
    DontDelete: false,
    Internal: false
  });
}
 
// 如果属性存在就设置值,但不改变attributes特性
O.P = V
 
return;

例如:

复制代码 代码如下:

Object.prototype.x = 100;
 
var foo = {};
console.log(foo.x); // 100, 继承属性
 
foo.x = 10; // [[Put]]
console.log(foo.x); // 10, 自身属性
 
delete foo.x;
console.log(foo.x); // 重新是100,继承属性
请注意,不能掩盖原型里的只读属性,赋值结果将忽略,这是由内部方法[[CanPut]]控制的。

// 例如,属性length是只读的,我们来掩盖一下length试试
 
function SuperString() {
  /* nothing */
}
 
SuperString.prototype = new String("abc");
 
var foo = new SuperString();
 
console.log(foo.length); // 3, "abc"的长度
 
// 尝试掩盖
foo.length = 5;
console.log(foo.length); // 依然是3

但在ES5的严格模式下,如果掩盖只读属性的话,会保存TypeError错误。

属性访问器

内部方法[[Get]]和[[Put]]在ECMAScript里是通过点符号或者索引法来激活的,如果属性标示符是合法的名字的话,可以通过“.”来访问,而索引方运行动态定义名称。

复制代码 代码如下:

var a = {testProperty: 10};
 
alert(a.testProperty); // 10, 点
alert(a['testProperty']); // 10, 索引
 
var propertyName = 'Property';
alert(a['test' propertyName]); // 10, 动态属性通过索引的方式

这里有一个非常重要的特性——属性访问器总是使用ToObject规范来对待“.”左边的值。这种隐式转化和这句“在JavaScript中一切都是对象”有关系,(然而,当我们已经知道了,JavaScript里不是所有的值都是对象)。

如果对原始值进行属性访问器取值,访问之前会先对原始值进行对象包装(包括原始值),然后通过包装的对象进行访问属性,属性访问以后,包装对象就会被删除。

例如:

复制代码 代码如下:

var a = 10; // 原始值
 
// 但是可以访问方法(就像对象一样)
alert(a.toString()); // "10"
 
// 此外,我们可以在a上创建一个心属性
a.test = 100; // 好像是没问题的
 
// 但,[[Get]]方法没有返回该属性的值,返回的却是undefined
alert(a.test); // undefined

那么,为什么整个例子里的原始值可以访问toString方法,而不能访问新创建的test属性呢?

答案很简单:

首先,正如我们所说,使用属性访问器以后,它已经不是原始值了,而是一个包装过的中间对象(整个例子是使用new Number(a)),而toString方法这时候是通过原型链查找到的:

复制代码 代码如下:

// 执行a.toString()的原理:  

  1. wrapper = new Number(a);
  2. wrapper.toString(); // "10"
  3. delete wrapper;

接下来,[[Put]]方法创建新属性时候,也是通过包装装的对象进行的:

复制代码 代码如下:

// 执行a.test = 100的原理:  

  1. wrapper = new Number(a);
  2. wrapper.test = 100;
  3. delete wrapper;

我们看到,在第3步的时候,包装的对象以及删除了,随着新创建的属性页被删除了——删除包装对象本身。

然后使用[[Get]]获取test值的时候,再一次创建了包装对象,但这时候包装的对象已经没有test属性了,所以返回的是undefined:

复制代码 代码如下:

// 执行a.test的原理:  

  1. wrapper = new Number(a);
  2. wrapper.test; // undefined

这种方式解释了原始值的读取方式,另外,任何原始值如果经常用在访问属性的话,时间效率考虑,都是直接用一个对象替代它;与此相反,如果不经常访问,或者只是用于计算的话,到可以保留这种形式。

继承

我们知道,ECMAScript是使用基于原型的委托式继承。链和原型在原型链里已经提到过了。其实,所有委托的实现和原型链的查找分析都浓缩到[[Get]]方法了。

如果你完全理解[[Get]]方法,那JavaScript中的继承这个问题将不解自答了。

经常在论坛上谈论JavaScript中的继承时,我都是用一行代码来展示,事实上,我们不需要创建任何对象或函数,因为该语言已经是基于继承的了,代码如下:

复制代码 代码如下:

alert(1..toString()); // "1"

我们已经知道了[[Get]]方法和属性访问器的原理了,我们来看看都发生了什么:

1.首先,从原始值1,通过new Number(1)创建包装对象
2.然后toString方法是从这个包装对象上继承得到的

为什么是继承的? 因为在ECMAScript中的对象可以有自己的属性,包装对象在这种情况下没有toString方法。 因此它是从原理里继承的,即Number.prototype。

注意有个微妙的地方,在上面的例子中的两个点不是一个错误。第一点是代表小数部分,第二个才是一个属性访问器:

复制代码 代码如下:

1.toString(); // 语法错误!
 
(1).toString(); // OK
 
1..toString(); // OK
 
1['toString'](); // OK

原型链

让我们展示如何为用户定义对象创建原型链,非常简单:

复制代码 代码如下:

function A() {
  alert('A.[[Call]] activated');
  this.x = 10;
}
A.prototype.y = 20;
 
var a = new A();
alert([a.x, a.y]); // 10 (自身), 20 (继承)
 
function B() {}
 
// 最近的原型链方式就是设置对象的原型为另外一个新对象
B.prototype = new A();
 
// 修复原型的constructor属性,否则的话是A了
B.prototype.constructor = B;
 
var b = new B();
alert([b.x, b.y]); // 10, 20, 2个都是继承的
 
// [[深远理解,浓密理解Javascript面向对象编制程序。Get]] b.x:
// b.x (no) -->
// b.[[Prototype]].x (yes) - 10
 
// [[Get]] b.y
// b.y (no) -->
// b.[[Prototype]].y (no) -->
// b.[[Prototype]].[[Prototype]].y (yes) - 20
 
// where b.[[Prototype]] === B.prototype,
// and b.[[Prototype]].[[Prototype]] === A.prototype

这种方法有两个特性:

首先,B.prototype将包含x属性。乍一看这可能不对,你可能会想x属性是在A里定义的并且B构造函数也是这样期望的。尽管原型继承正常情况是没问题的,但B构造函数有时候可能不需要x属性,与基于class的继承相比,所有的属性都复制到后代子类里了。

尽管如此,如果有需要(模拟基于类的继承)将x属性赋给B构造函数创建的对象上,有一些方法,我们后来来展示其中一种方式。

其次,这不是一个特征而是缺点——子类原型创建的时候,构造函数的代码也执行了,我们可以看到消息"A.[[Call]] activated"显示了两次——当用A构造函数创建对象赋给B.prototype属性的时候,另外一场是a对象创建自身的时候!

下面的例子比较关键,在父类的构造函数抛出的异常:可能实际对象创建的时候需要检查吧,但很明显,同样的case,也就是就是使用这些父对象作为原型的时候就会出错。

复制代码 代码如下:

function A(param) {
  if (!param) {
    throw 'Param required';
  }
  this.param = param;
}
A.prototype.x = 10;
 
var a = new A(20);
alert([a.x, a.param]); // 10, 20
 
function B() {}
B.prototype = new A(); // Error

此外,在父类的构造函数有太多代码的话也是一种缺点。

解决这些“功能”和问题,程序员使用原型链的标准模式(下面展示),主要目的就是在中间包装构造函数的创建,这些包装构造函数的链里包含需要的原型。

复制代码 代码如下:

function A() {
  alert('A.[[Call]] activated');
  this.x = 10;
}
A.prototype.y = 20;
 
var a = new A();
alert([a.x, a.y]); // 10 (自身), 20 (集成)
 
function B() {
  // 或者使用A.apply(this, arguments)
  B.superproto.constructor.apply(this, arguments);
}
 
// 继承:通过空的中间构造函数将原型连在一起
var F = function () {};
F.prototype = A.prototype; // 引用
B.prototype = new F();
B.superproto = A.prototype; // 显示引用到另外一个原型上, "sugar"
 
// 修复原型的constructor属性,否则的就是A了
B.prototype.constructor = B;
 
var b = new B();
alert([b.x, b.y]); // 10 (自身), 20 (集成)

注意,我们在b实例上创建了自己的x属性,通过B.superproto.constructor调用父构造函数来引用新创建对象的上下文。

我们也修复了父构造函数在创建子原型的时候不需要的调用,此时,消息"A.[[Call]] activated"在需要的时候才会显示。

为了在原型链里重复相同的行为(中间构造函数创建,设置superproto,恢复原始构造函数),下面的模板可以封装成一个非常方面的工具函数,其目的是连接原型的时候不是根据构造函数的实际名称。

复制代码 代码如下:

function inherit(child, parent) {
  var F = function () {};
  F.prototype = parent.prototype
  child.prototype = new F();
  child.prototype.constructor = child;
  child.superproto = parent.prototype;
  return child;
}

因此,继承:

复制代码 代码如下:

function A() {}
A.prototype.x = 10;
 
function B() {}
inherit(B, A); // 连接原型
 
var b = new B();
alert(b.x); // 10, 在A.prototype查找到

也有很多语法形式(包装而成),但所有的语法行都是为了减少上述代码里的行为。

例如,如果我们把中间的构造函数放到外面,就可以优化前面的代码(因此,只有一个函数被创建),然后重用它:

复制代码 代码如下:

var inherit = (function(){
  function F() {}
  return function (child, parent) {
    F.prototype = parent.prototype;
    child.prototype = new F;
    child.prototype.constructor = child;
    child.superproto = parent.prototype;
    return child;
  };
})();

由于对象的真实原型是[[Prototype]]属性,这意味着F.prototype可以很容易修改和重用,因为通过new F创建的child.prototype可以从child.prototype的当前值里获取[[Prototype]]:

复制代码 代码如下:

function A() {}
A.prototype.x = 10;
 
function B() {}
inherit(B, A);
 
B.prototype.y = 20;
 
B.prototype.foo = function () {
  alert("B#foo");
};
 
var b = new B();
alert(b.x); // 10, 在A.prototype里查到
 
function C() {}
inherit(C, B);
 
// 使用"superproto"语法糖
// 调用父原型的同名方法
 
C.ptototype.foo = function () {
  C.superproto.foo.call(this);
  alert("C#foo");
};
 
var c = new C();
alert([c.x, c.y]); // 10, 20
 
c.foo(); // B#foo, C#foo

注意,ES5为原型链标准化了这个工具函数,那就是Object.create方法。ES3可以使用以下方式实现:

复制代码 代码如下:

Object.create ||
Object.create = function (parent, properties) {
  function F() {}
  F.prototype = parent;
  var child = new F;
  for (var k in properties) {
    child[k] = properties[k].value;
  }
  return child;
}

// 用法
var foo = {x: 10};
var bar = Object.create(foo, {y: {value: 20}});
console.log(bar.x, bar.y); // 10, 20

此外,所有模仿现在基于类的经典继承方式都是根据这个原则实现的,现在可以看到,它实际上不是基于类的继承,而是连接原型的一个很方便的代码重用。

结论

本章内容已经很充分和详细了,希望这些资料对你有用,并且消除你对ECMAScript的疑问,如果你有任何问题,请留言,我们一起讨论。

本文由澳门新萄京官方网站发布于澳门新萄京赌场网址,转载请注明出处:深远理解,浓密理解Javascript面向对象编制程序

关键词: