深入理解Javascript中构造函数和原型对象的区别
在 Javascript中prototype属性的详解 这篇文章中,详细介绍了构造函数的缺点以及原型(prototype),原型链(prototype chain),构造函数(constructor),instanceof运算符的一些特点。如果对prototype和构造函数不熟悉,可以前往Javascript中prototype属性的详解 和 Javascript 中构造函数与new命令的密切关系 仔细的品味品味。先来做一个简单的回顾。
首先,我们知道,构造函数是生成对象的模板,一个构造函数可以生成多个对象,每个对象都有相同的结构。构造函数的缺点就是,每当你实例化两个对象时,需要调用两次构造函数的某一个方法,这带来的坏处就是占用内存,而且没必要。
其次,为了解决构造函数的属性和方法无法被对象实例所共享的问题,我们可以把需要共享的属性和方法放在原型(prototype)对象上。原型对象上的所有属性和方法,都会被对象实例所共享。对于构造函数来说,prototype是作为构造函数的属性;对于对象实例来说,prototype是对象实例的原型对象。所以prototype即是属性,又是对象。
然后,除了undefined和null之外,每一个数据类型都可以看成一个对象,每一个对象都有它的原型。所有一切对象的原型顶端,都是Object.prototype,即Object构造函数的prototype属性指向的那个对象。当然,Object.prototype对象也有自己的原型对象,那就是没有任何属性和方法的null对象,而null对象没有自己的原型。
原型链的特点有:
a:读取对象的某个属性时,JavaScript引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype
还是找不到,则返回undefined
。
b:如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overiding)。
c:一级级向上在原型链寻找某个属性,对性能是有影响的。所寻找的属性在越上层的原型对象,对性能的影响越大。如果寻找某个不存在的属性,将会遍历整个原型链。
再次,constructor属性是原型对象上的一个属性,可以被所有实例对象所共享。要注意的是,prototype是构造函数的属性,而constructor则是构造函数的prototype属性所指向的那个对象,也就是原型对象的属性。由于constructor属性是一种原型对象和构造函数的关系,所以在修改原型对象的时候,一定要注意constructor的指向问题。
最后,instanceof运算符返回一个布尔值,用于判断对象是否为某个构造函数的实例。
在接下来的分享中,会谈谈Object的部分方法和Object.prototoype的部分方法。虽然都是概念性问题,但是如果理解了这些概念,对于MVVM框架和各种js框架的理解都有相当大的帮助。
以下的分享会分为如下内容:
1.Object和Object.prototype的区别
2.Object.getPrototypeOf()
3.Object.setPrototypeOf()
4.Object.create()
5.Object.prototype.isPrototypeOf()
6.Object.prototype.__proto__
1.Object和Object.prototype的区别
个人认为,要学好javascript的其中一个方法就是,必须理解每一个" . "所代表的意思是什么,是调用自身的属性和方法呢,还是继承原型的对象的属性和方法。来看看Object构造函数和构造函数的原型Object.prototype都有哪些属性和方法。
Object是构造函数,而Object.prototype是构造函数的原型对象。构造函数自身的属性和方法无法被共享,而原型对象的属性和方法可以被所有实例对象所共享。
Object的属性和方法:
Object.prototype的属性和方法:
上面例子中,Object拥有自己的方法prototype,getPrototypeOf(),setPrototypeOf()等,这些方法无法被实例所共享。而Object.prototypeOf()的hasOwnProperty,isPrototypeOf(),constructor等属性和方法是可以被实例对象所共享的。举一个最简单的例子。
1 function Keith() {} 2 var a = new Keith(); 3 console.log(a.prototype); //undefined 4 console.log(a.constructor); //Keith()
上面代码中,构造函数Keith是没有任何属性和方法的。当访问prototype属性时返回undefined,是因为prototype属性没有办法从构造函数中继承,只能由构造函数本身访问。而constructor返回了Keith(),因为constructor属性本身就是Object.prototype中的属性,可以被所有实例对象所共享。
那么问题来了,如何知道实例对象的原型呢?可以通过Object.isPrototypeOf方法和继承原型对象的isPrototypeOf方法实现。
1 console.log(Keith.prototype.isPrototypeOf(a)); //true 2 console.log(Object.getPrototypeOf(a) === Keith.prototype) //true
上面代码中,实例对象a的原型就是Keith.prototype。这两个属性会稍后介绍。
2.Object.getPrototypeOf()
Object.getPrototypeOf
方法返回一个对象的原型。这是获取原型对象的标准方法。
1 // 空对象的原型是Object.prototype 2 console.log(Object.getPrototypeOf({}) === Object.prototype) // true 3 4 // 函数的原型Function.prototype 5 function keith() {} 6 console.log(Object.getPrototypeOf(keith) === Function.prototype) //true 7 8 // 数组的原型Array.prototype 9 var arr = [1,2,3]; 10 console.log(Object.getPrototypeOf(arr) === Array.prototype) ///true
3.Object.setPrototypeOf()
Object.setPrototypeOf方法可以为现有对象设置原型,然后返回一个新对象。这个可以接收两个参数,第一个是现有对象,第二个是原型对象。
1 var keith = { 2 height: 180 3 }; 4 var rascal = Object.setPrototypeOf({}, keith); 5 console.log(rascal.height); //180 6 7 //上下两个代码片段相同。 8 var keith = { 9 height: 180 10 }; 11 var rascal ={ 12 __proto__: keith 13 }; 14 console.log(rascal.height); //180
上面代码中,rascal对象是Object.setPrototypeOf
方法返回的一个新对象。该对象本身为空、原型为keith对象,所以rascal对象可以拿到keith对象的所有属性和方法。rascal对象本身并没有height属性,但是JavaScript引擎找到它的原型对象keith,然后读取keith的height属性。
4.Object.create()
Object.create方法用于从原型对象生成新的对象实例,可以代替new命令。它接受一个参数,这个参数为所要继承的原型对象,然后返回一个实例对象。
1 var Keith = { 2 hobby : function() { 3 return 'Watching Movies'; 4 } 5 }; 6 7 var rascal = Object.create(Keith); 8 console.log(rascal.hobby()) //'Watching Movies'
上面代码中,Object.create方法将Keith对象作为rascal的原型对象,此时rascal就继承了Keith对象中的所有属性和方法。rascal就成为了Keith对象的实例对象。用下面这段代码比较好理解。
1 function Keith() {}; 2 Keith.prototype.hobby = function() { 3 return 'Watching Movies'; 4 } 5 6 var rascal = Object.create(Keith); 7 console.log(rascal.hobby()) //'Watching Movies';
new操作符和Object.create方法都是返回一个对象实例,但是两者有一些区别。
1 function Keith() {} 2 var a = new Keith(); 3 var b = Object.create(Keith.prototype); 4 5 console.log(a instanceof Keith); //true 6 console.log(b instanceof Keith); //true
上面代码中,可以使用new操作符来调用构造函数,返回对象实例;而Object.create传入的参数必须是构造函数Keith的原型。
实际上,如果有老式浏览器不支持Object.create方法,可以用下面这段代码来构造一个Object.create方法。
1 if (typeof Object.create !=='function') { 2 Object.create = function(x) { 3 function F() {}; 4 F.prototype = x; 5 return new F(); 6 }; 7 }
下面这三种方式生成的实例对象都是等价的。
1 var o1 = Object.create({}); 2 var o2 = Object.create(Object.prototype); 3 var o2 = new Object();
在使用Object.create方法时,要注意的是必须传入原型对象,否则会报错。
1 var o1 = Object.create(); 2 console.log(o1);//TypeError: Object.create requires more than 0 arguments
Object.create方法生成的对象实例,动态继承了原型对象。也就是说,修改原型对象的属性和方法会反应在对象实例上。
1 var keith = { 2 height:180 3 }; 4 5 var rascal = Object.create(keith); 6 keith.height=153; 7 console.log(rascal.height) //153
上面代码中,修改原型对象,会影响生成的对象实例。
Object.create方法生成的对象,继承了它的原型对象的构造函数。
1 function Keith() {}; 2 var boy = new Keith(); 3 var girl = Object.create(boy); 4 console.log(Object.getPrototypeOf(girl) === boy); //true 5 console.log(girl.constructor === Keith); //true 6 console.log(girl instanceof Keith); //true
上面代码中,girl对象的原型是boy对象,girl对象的constructor属性指向了原型对象boy的构造函数Keith。
5.Object.prototype.isPrototypeOf()
对象实例的isPrototypeOf方法,用于判断一个对象对象是否是另外一个对象的原型。
1 var o1 = {}; 2 var o2 = Object.create(o1); 3 var o3 = Object.create(o2); 4 5 console.log(o1.isPrototypeOf(o2)); //true 6 console.log(o2.isPrototypeOf(o3)); //true 7 console.log(o1.isPrototypeOf(o3)); //true
上面代码中,可以看出,只要某个对象处于原型链上,isPrototypeOf都返回true。
1 function Keith() {}; 2 3 console.log(Function.prototype.isPrototypeOf(Keith)); //true 4 console.log(Object.prototype.isPrototypeOf(Function)); //true 5 console.log(Object.getPrototypeOf(Object.prototype) === null); //true
上面代码中,构造函数Keith的原型指向了Function.prototype,而构造函数Function的原型指向了Object.prototype。Object的原型指向了没有任何属性和方法的null对象。
6.Object.prototype.__proto__
__proto__属性(前后两条下划线)可以改写某个对象的原型对象。这个属于实例方法。
1 var keith = {}; 2 var rascal = {}; 3 rascal.__proto__ = keith; 4 console.log(keith.isPrototypeOf(rascal)); //true
上面代码中,通过rascal对象的__proto__属性,将rascal的原型指向了keith对象。
__proto__
属性只有浏览器才需要部署,其他环境可以没有这个属性,而且前后的两根下划线,表示它本质是一个内部属性,不应该对使用者暴露。因此,应该尽量少用这个属性,而是用Object.getPrototypeof()
(读取)和Object.setPrototypeOf()
(设置),进行原型对象的读写操作。
来做一个小小的总结,上面对一些属性和方法的介绍都可以归结为一句话:
构造函数本身的属性无法被对象实例共享,而原型对象上的属性和方法可以被所用对象实例所共享。
完。
最近在读一本进阶的JavaScript的书《你不知道的JavaScript(上卷)》,里面分析了很多基础性的概念。
可以更全面深入的理解JavaScript深层面的知识点。
一、函数作用域
1)函数作用域
就是作用域在一个“Function”里,属于这个函数的全部变量都可以在整个函数的范围内使用及复用。
function foo(a) { var b = 2; function bar() { // ... } var c = 3; } bar(); // 失败 console.log( a, b, c ); // 三个全都失败
上面的“foo”函数内的几个标识符,放到函数外面访问就都会报错,查看源码。
2)立即执行函数表达式
在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容。
例如上面的bar、a等几个标识符。这样能够保护变量不被污染。
在写插件的时候经常会用到立即执行函数表达式,为的就是保护里面的变量。
var a = 2; (function foo() { var a = 3; console.log( a ); // 3 })(); console.log( a ); // 2
“foo”中第一个( )将函数变成表达式,第二个( )执行了这个函数。
有一个专用术语:IIFE,代表立即执行函数表达式(Immediately Invoked Function Expression);
1. 进阶用法是把它们当作函数调用并传递参数进去
(function IIFE( global ) { var a = 3; console.log( a ); // 3 console.log( global.a ); // 2 })( window );
2. 一种变化的用途是倒置代码的运行顺序,在CMD或AMD项目中被广泛使用。
(function IIFE(factory) { factory( window ); })(function def( global ) { var a = 3; console.log( a ); // 3 console.log( global.a ); // 2 });
二、块作用域
JavaScript不支持块作用域。
for(var i=0; i<10; i++) { console.log( i ); }
上面的代码中的“i”相当于下面的
var i; for(i=0; i<10; i++) { console.log( i ); }
但也有例外,“try/catch”,catch就是一个块作用域。
try{ undefined(); // 执行一个非法操作来强制制造一个异常 } catch(err) { console.log( err ); // 能够正常执行! } console.log( err ); // ReferenceError: err not found
ES6改变了现状,引入了新的let关键字,let关键字可以将变量绑定到所在的任意作用域中(通常是{ .. }内部)。换句话说,let为其声明的变量隐式地了所在的块作用域。
三、提升
函数作用域和块作用域的行为是一样的,可以总结为:任何声明在某个作用域内的变量,都将附属于这个作用域。
1)编译与执行
变量和函数的所有声明都会在任何代码被执行前首先被处理,可以看下面的代码事例。
a = 2; var a; console.log(a);//2
这段代码等价于:
var a;//定义声明是在编译阶段进行 a = 2;//赋值声明会被留在原地等待执行阶段 console.log(a);
2)函数优先
函数会首先被提升,然后才是变量。
foo(); // 1 var foo; function foo() { console.log( 1 ); } foo = function() { console.log( 2 ); };
var foo函数表达式尽管出现在function foo()的声明之前,但它是重复的声明(因此被忽略了),因为函数声明会被提升到普通变量之前。
而上面的代码相当于:
function foo() { console.log( 1 ); } foo(); // 1 foo = function() { console.log( 2 ); };
四、闭包
1)定义
当函数可以记住并访问所在的作用域时,就产生了闭包,即使函数是在当前作用域之外执行。
function foo() { var a = 2; function bar() { console.log( a ); } return bar; } var baz = foo(); baz(); // 2 —— 这就是闭包的效果。
1. 将函数“bar”赋值给“baz”,执行“baz”,当前作用域并不在“bar”的作用域,但是可以执行。
2. 闭包还会阻止垃圾回收,当“foo”执行完后,内部作用域仍然存在。这样才能让“baz”执行。
2)将函数作为参数传递
function foo() { var a = 2; function baz() { console.log( a ); // 2 } bar( baz ); } function bar(fn) { fn(); //这就是闭包! }
把内部函数baz传递给bar,当调用这个内部函数时(fn),它涵盖的foo()内部作用域的闭包就可以观察到了,因为它能够访问a。
如果将函数当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。
在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
3)循环和闭包
for (var i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i); }, i * 1000); }
每次打印出来都将会是6,延迟函数的回调会在循环结束时才执行,查看源码。
根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i。
现在用闭包来实现每次打印不同的i。
for (var i = 1; i <= 5; i++) { (function(j) { setTimeout(function timer() { console.log(j); }, j * 1000); })(i); }
IIFE会通过声明并立即执行一个函数来创建作用域。setTimeout中的回调可以记住当前的作用域,每个作用域中的参数“j”都是不同的。