(注1:如果有问题欢迎留言探讨,一起学习!转载请注明出处,喜欢可以点个赞哦!)
(注2:更多内容请查看我的目录。)
1. 简介
在前面,我们对这三个概念已经有所涉及,但是却并未深究。事实上,如果能熟练理解掌握这三个概念和他们之间的关系,那么在学习原型链和继承的知识时,会有一种拨云见雾之感。
2. 构造函数
构造函数其实与普通函数本身并无区别,普通函数通过new调用时,我们就称其为构造函数。当然,为了区分其与普通函数,构造函数约定首字母需要大写。下面,我们就来看一下构造函数和普通函数使用时的区别(简单来讲就是一个函数通过new调用和不通过new调用的区别)。
2.1 一个空函数
// 空函数
function A() {}
var a1 = A();
var a2 = new A();
console.log('a1:', a1); //undefined
console.log('a2:', a2); //{}
在chrome的控制台console运行结果如图所示:
直接调用返回undefined,而使用new调用返回的却是一个空对象。这里,我们暂且不去讨论_proto_和constructor的含义。
2.2 无this有return,但是return后面无返回值,或者返回基本类型值。
// 无返回值
function A() {
return;
}
//返回undefined类型值
function B() {
return undefined;
}
// 返回Number类型值
function C() {
return 1;
}
// 返回String类型值
function D() {
return '1';
}
// 返回Boolean类型值
function E() {
return true;
}
// 返回Null类型值
function F() {
return null;
}
var a1 = A();
var a2 = new A();
console.log('a1:', a1);
console.log('a2:', a2);
var b1 = B();
var b2 = new A();
console.log('b1:', b1);
console.log('b2:', b2);
var c1 = C();
var c2 = new A();
console.log('c1:', c1);
console.log('c2:', c2);
var d1 = D();
var d2 = new A();
console.log('d1:', d1);
console.log('d2:', d2);
var e1 = E();
var e2 = new A();
console.log('e1:', e1);
console.log('e2:', e2);
var f1 = F();
var f2 = new A();
console.log('f1:', f1);
console.log('f2:', f2);
2.2
可以看到,普通调用会返回return后面的值,而new调用返回空对象{}。
2.3 无this有return,但是return后面是一个对象(包括函数)。
// 返回对象
function A() {
return {m: 1};
}
//返回函数
function B() {
return function () {
return 123;
}
}
var a1 = A();
var a2 = new A();
console.log('a1:', a1);
console.log('a2:', a2);
var b1 = B();
var b2 = new B();
console.log('b1:', b1);
console.log('b2:', b2);
2.3
可以看出,不管是普通调用还是new调用都是返回return后面的值。
2.4 有this,无return。
function A() {
this.m = 1;
this.n = function () {
console.log(123);
};
}
var a1 = A();
var a2 = new A();
console.log('a1:', a1);
console.log('a2:', a2);
2.4
普通调用返回undefined,而new调用返回一个对象,构造函数A中的this指向了该对象,所以返回对象的属性和方法由构造函数中的this语句初始化。
ps: 需要注意的是,普通调用的时候,this指向了undefined,非严格模式下指向了widow。
2.5 有this,有return。但是return后面无返回值,或者返回基本类型值。
// 无返回值
function A() {
this.m = 1;
return;
}
//返回undefined类型值
function B() {
this.m = 1;
return undefined;
}
// 返回Number类型值
function C() {
this.m = 1;
return 1;
}
// 返回String类型值
function D() {
this.m = 1;
return '1';
}
// 返回Boolean类型值
function E() {
this.m = 1;
return true;
}
// 返回Null类型值
function F() {
this.m = 1;
return null;
}
var a1 = A();
var a2 = new A();
console.log('a1:', a1);
console.log('a2:', a2);
var b1 = B();
var b2 = new B();
console.log('b1:', b1);
console.log('b2:', b2);
var c1 = C();
var c2 = new C();
console.log('c1:', c1);
console.log('c2:', c2);
var d1 = D();
var d2 = new D();
console.log('d1:', d1);
console.log('d2:', d2);
var e1 = E();
var e2 = new E();
console.log('e1:', e1);
console.log('e2:', e2);
var f1 = F();
var f2 = new F();
console.log('f1:', f1);
console.log('f2:', f2);
2.5
可以看到,普通调用会返回return后面的值,而new调用返回一个对象,构造函数A中的this指向了该对象,所以返回对象的属性和方法由构造函数中的this语句初始化。
ps: 需要注意的是,普通调用的时候,this指向了undefined,非严格模式下指向了widow。
2.6 有this,有return。return后面是一个对象(包括函数)。
// 返回对象
function A() {
this.m = 1;
return {n: 2};
}
//返回函数
function B() {
this.f = function () {
return 1;
};
return function () {
return 2;
}
}
var a1 = A();
var a2 = new A();
console.log('a1:', a1);
console.log('a2:', a2);
var b1 = B();
var b2 = new B();
console.log('b1:', b1);
console.log('b2:', b2);
2.6
可以看到,不管是普通调用还是new调用都是返回return后面的值。
ps: 需要注意的是,普通调用的时候,this指向了undefined,非严格模式下指向了widow。
总结:对于构造函数调用,有如下特点:
- 如果没有return,返回一个新的对象,构造函数的this指向该对象。
- 如果有return且后面的返回值不是对象(包括函数),则return语句会被忽略。
- 如果有return且后面返回一个对象(包括函数),则返回该对象。
3. 实例对象
第2节我们已经阐述了构造函数的定义和使用方法,现在我们来看一下实例对象的定义。
实例对象:通过构造函数的new操作创建的对象是实例对象,又常常被称为对象实例。可以用一个构造函数,构造多个实例对象。下面的f1和f2就是实例对象。
function Foo(){};
var f1 = new Foo;
var f2 = new Foo;
console.log(f1 === f2);//false
4. 原型对象
首先,我们来看两段《JavaScrpit高级程序设计》对原型模式和原型对象的阐述:
我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么prototype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。
简而言之,任何一个函数,都拥有一个prototype属性,指向其原型对象,该原型对象也是由该函数new调用创造的所有实例对象的原型对象。
5. 构造函数,原型对象和实例对象的关系
5.1 指向关系
构造函数A的prototype属性指向F与其实例对象(a1,a2,...)的原型对象A.prototype,该原型对象的constructor属性指向构造函数A,实例对象拥有[[Prototype]]属性(在firefox,safari和chrome上该属性实现为_proto_)指向原型对象A.prototype
function A() {
}
var a1 = new A();
var a2 = new A();
5.1
还记得我们在前面2.1节的空函数为构造函数的图片吗?现在来看是不是就很清晰了。明白了其中的指向关系,我们再来看一下,构造函数中添加this语句以及在原型对象中添加属性以后是怎样的情况。
5.2 实例化时的数据关系
// 代码段5.2
function A() {
this.m = 1;
this.n = [1, 2];
}
A.prototype.p = 2;
A.prototype.q = [3, 4];
var a1 = new A();
var a2 = new A();
当使用构造函数新建实例对象时,各个实例对象都会拥有由this指定的属性。
5.2
5.3 实例对象属性赋值和使用时的关系(可以类比LHS和RHS)
5.3.1 使用时的继承关系
使用实例对象属性时,如果该属性不存在于实例对象,就会使用其原型对象该属性。
在代码段5.2执行之后做如下操作:
// 代码段5.3.1,承接代码段5.2
console.log('a1.m:', a1.m);
console.log('a2.m:', a2.m);
console.log('a1.n:', a1.n);
console.log('a2.n:', a2.n);
console.log('a1.p:', a1.p);
console.log('a2.p:', a2.p);
console.log('a1.q:', a1.q);
console.log('a2.q:', a2.q);
5.3.1
如图所示,打印a1.m会找到其实例对象属性m,而a1.p会找到其原型对象属性p。
5.3.2 使用查找时的先后关系(赋值时的覆盖关系)
使用实例对象属性时,优先从实例对象查找该属性,如果该属性不存在,就会使用其原型对象该属性。而对实例对象属性的赋值操作,将会直接使用实例对象属性。
// 代码段5.3.2.1,承接代码段5.3.1
a1.p = 11;
console.log('a1.p:', a1.p);
console.log('a2.p:', a2.p);
5.3.2.1
说明,a1.p是给a1添加了属性p并赋值11,但是此时a2是没有该属性的,所以对a2.p的使用会查找到A.prototype。
要注意的是,这里实例对象属性之间是互相独立的,而原型对象属性是共享的。
// 代码段5.3.2.2,承接代码段5.3.2.1
a1.n.push(3);
a1.q.push(5);
console.log('a1.n:', a1.n);
console.log('a2.n:', a2.n);
console.log('a1.q:', a1.q);
console.log('a2.q:', a2.q);
5.3.2.2
可以看到,对原型对象属性为对象时的操作( 堆操作)会影响到其他的实例对象对该属性的使用。
另外,还有一点要注意,如果你对对象使用的是赋值操作,并不会影响到原型属性。不明白的同学再看一下5.3.2.1。
6. 总结
其实,我们用代码解释一下new函数构造一个实例的过程。
对于
function A(m, n) {
this.m = m;
this.n = n;
}
var a = new A(1, 2);
console.log(a);
6.1
中的 new A(1,2)这一步操作,其实可以分解为如下四个步骤:
// 新建一个空对象obj
let obj ={};
// obj的__proto__属性指向原型对象
obj.__proto__ = A.prototype;
// 将构造函数的this绑定obj,传入构造函数的参数,并将返回结果赋值给result
let result = A.apply(obj, arguments);
// 如果result存在且result是对象或者函数,则构造函数返回result,否则将返回obj
return (result && (typeof(result) === 'object' || typeof(result) === 'function')?result:obj);
- 新建一个空对象obj
- obj的proto属性指向原型对象
- 将构造函数的this绑定obj,传入构造函数的参数,并将返回结果赋值给result
- 如果result存在且result是对象或者函数,则构造函数返回result,否则将返回obj
我们可以试着模拟一个函数myNewA,如下:
function A(m, n) {
this.m = m;
this.n = n;
}
function myNewA() {
let obj ={};
obj.__proto__ = A.prototype;
let result = A.apply(obj, arguments);
return (result && (typeof(result) === 'object' || typeof(result) === 'fucntion')?result:obj);
}
var a = myNewA(1, 2)
console.log(a);
6.2
可以看到,结果和6.1一模一样,当然了,真正的new构造函数的过程不会是这么简单,我们只是通过这个例子使大家能够加深对构造函数,原型对象和实例对象的理解。
参考
javascript面向对象系列第一篇——构造函数和原型对象
JS入门难点解析10-创建对象
深入理解js构造函数
JavaScript构造函数详解
BOOK-《JavaScript高级程序设计(第3版)》第6章
网友评论