在典型的面向对象的语言中,如 java,都存在类(class)的概念,类就是对象的模板,对象就是类的实例。但是在 Javascript 语言体系中,是不存在类(Class)的概念的,javascript 中不是基于'类的',而是通过构造函数(constructor)和原型链(prototype chains)实现的。但是在 ES6 中提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过
关键字,可以定义类。基本上,ES6 的
- class
可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的
- class
写法只是让原型对象的写法更加清晰、更像面向对象编程的语法而已。
- class
按照我的习惯,在写文章前我会给出文章目录。
以下内容会分为如下小节:
1. 构造函数的简单介绍
2. 构造函数的缺点
3.prototype 属性的作用
4. 原型链(prototype chain)
5.constructor 属性
5.1:constructor 属性的作用
6.instanceof 运算符
1. 构造函数的简单介绍
在我的一篇文章中,详细了介绍了构造函数的概念和特点,new 命令的原理和用法等,如果对于构造函数不熟悉的同学,可以前往细细品味。以下做一个简单的回顾。
所谓构造函数,就是提供了一个生成对象的模板并描述对象的基本结构的函数。一个构造函数,可以生成多个对象,每个对象都有相同的结构。总的来说,构造函数就是对象的模板,对象就是构造函数的实例。
构造函数的特点有:
a:构造函数的函数名首字母必须大写。
b:内部使用 this 对象,来指向将要生成的对象实例。
c:使用 new 操作符来调用构造函数,并返回对象实例。
看一个最简单的一个例子。
- 1
- function Person() {
- 2 this.name = 'keith';
- 3
- }
- 4 5
- var boy = new Person();
- 6 console.log(boy.name); //'keith'
2. 构造函数的缺点
所有的实例对象都可以继承构造函数中的属性和方法。但是,同一个对象实例之间,无法共享属性。
- 1
- function Person(name, height) {
- 2 this.name = name;
- 3 this.height = height;
- 4 this.hobby = function() {
- 5
- return 'watching movies';
- 6
- }
- 7
- }
- 8 9
- var boy = new Person('keith', 180);
- 10
- var girl = new Person('rascal', 153);
- 11 12 console.log(boy.name); //'keith'
- 13 console.log(girl.name); //'rascal'
- 14 console.log(boy.hobby === girl.hobby); //false
上面代码中,一个构造函数 Person 生成了两个对象实例 boy 和 girl,并且有两个属性和一个方法。但是,它们的 hobby 方法是不一样的。也就是说,每当你使用 new 来调用构造函数放回一个对象实例的时候,都会创建一个 hobby 方法。这既没有必要,又浪费资源,因为所有 hobby 方法都是童颜的行为,完全可以被两个对象实例共享。
所以,构造函数的缺点就是:同一个构造函数的对象实例之间无法共享属性或方法。
3.prototype 属性的作用
为了解决构造函数的对象实例之间无法共享属性的缺点,js 提供了 prototype 属性。
js 中每个数据类型都是对象(除了 null 和 undefined),而每个对象都继承自另外一个对象,后者称为 "原型"(prototype)对象,只有 null 除外,它没有自己的原型对象。
原型对象上的所有属性和方法,都会被对象实例所共享。
通过构造函数生成对象实例时,会将对象实例的原型指向构造函数的 prototype 属性。每一个构造函数都有一个 prototype 属性,这个属性就是对象实例的原型对象。
- 1
- function Person(name, height) {
- 2 this.name = name;
- 3 this.height = height;
- 4
- }
- 5 6 Person.prototype.hobby = function() {
- 7
- return 'watching movies';
- 8
- }
- 9 10
- var boy = new Person('keith', 180);
- 11
- var girl = new Person('rascal', 153);
- 12 13 console.log(boy.name); //'keith'
- 14 console.log(girl.name); //'rascal'
- 15 console.log(boy.hobby === girl.hobby); //true
上面代码中,如果将 hobby 方法放在原型对象上,那么两个实例对象都共享着同一个方法。我希望大家都能理解的是,对于构造函数来说,prototype 是作为构造函数的属性;对于对象实例来说,prototype 是对象实例的原型对象。所以 prototype 即是属性,又是对象。
原型对象的属性不是对象实例的属性。对象实例的属性是继承自构造函数定义的属性,因为构造函数内部有一个 this 关键字来指向将要生成的对象实例。对象实例的属性,其实就是构造函数内部定义的属性。只要修改原型对象上的属性和方法,变动就会立刻体现在所有对象实例上。
- 1 Person.prototype.hobby = function() {
- 2
- return 'swimming';
- 3
- }
- 4 console.log(boy.hobby === girl.hobby); //true
- 5 console.log(boy.hobby()); //'swimming'
- 6 console.log(girl.hobby()); //'swimming'
上面代码中,当修改了原型对象的 hobby 方法之后,两个对象实例都发生了变化。这是因为对象实例其实是没有 hobby 方法,都是读取原型对象的 hobby 方法。也就是说,当某个对象实例没有该属性和方法时,就会到原型对象上去查找。如果实例对象自身有某个属性或方法,就不会去原型对象上查找。
- 1 boy.hobby = function() {
- 2
- return 'play basketball';
- 3
- }
- 4 console.log(boy.hobby()); //'play basketball'
- 5 console.log(girl.hobby()); //'swimming'
上面代码中,boy 对象实例的 hobby 方法修改时,就不会在继承原型对象上的 hobby 方法了。不过 girl 仍然会继承原型对象的方法。
总结一下:
a:原型对象的作用,就是定义所有对象实例所共享的属性和方法。
b:prototype,对于构造函数来说,它是一个属性;对于对象实例来说,它是一个原型对象。
4. 原型链(prototype chains)
对象的属性和方法,有可能是定义在自身,也有可能是定义在它的原型对象。由于原型对象本身对于对象实例来说也是对象,它也有自己的原型,所以形成了一条原型链(prototype chain)。比如,
对象是
- a
对象的原型,
- b
对象是
- b
对象的原型,以此类推。所有一切的对象的原型顶端,都是 Object.prototype,即 Object 构造函数的 prototype 属性指向的那个对象。
- c
当然,Object.prototype 对象也有自己的原型对象,那就是没有任何属性和方法的 null 对象,而 null 对象没有自己的原型。
- 1 console.log(Object.getPrototypeOf(Object.prototype)); //null
- 2 console.log(Person.prototype.isPrototypeOf(boy)) //true
原型链(prototype chain)的特点有:
a:读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的
还是找不到,则返回
- Object.prototype
。
- undefined
b:如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做 "覆盖"(overiding)。
c:一级级向上在原型链寻找某个属性,对性能是有影响的。所寻找的属性在越上层的原型对象,对性能的影响越大。如果寻找某个不存在的属性,将会遍历整个原型链。
看概念可能比较晦涩,我们来看一个例子。但是理解了概念真的很重要。
- 1
- var arr = [1, 2, 3];
- 2 console.log(arr.length); //3
- 3 console.log(arr.valueOf()) //[1,2,3]
- 4 console.log(arr.join('|')) //1|2|3
上面代码中,定了一个数组 arr,数组里面有三个元素。我们并没有给数组添加任何属性和方法,可是却在调用 length,join(),valueOf() 时,却不会报错。
length 属性是继承自 Array.prototype 的,属于原型对象上的一个属性。join 方法也是继承自 Array.prototype 的,属于原型对象上的一个方法。这两个方法是所有数组所共享的。当实例对象上没有这个 length 属性时,就会去原型对象查找。
valueOf 方法是继承自 Object.prototype 的。首先,arr 数组是没有 valueOf 方法的,所以就到原型对象 Array.prototype 查找。然后,发现 Array.prototype 对象上没有 valueOf 方法。最后,再到它的原型对象 Object.prototype 查找。
来看看 Array.prototype 对象和 Object.prototype 对象分别有什么属性和方法。
- 1 console.log(Object.getOwnPropertyNames(Array.prototype)) 2 //["length", "toSource", "toString", "toLocaleString", "join", "reverse", "sort", "push", "pop", "shift", "unshift", "splice", "concat", "slice", "lastIndexOf", "indexOf", "forEach", "map", "filter", "reduce", "reduceRight", "some", "every", "find", "findIndex", "copyWithin", "fill", "entries", "keys", "values", "includes", "constructor", "$set", "$remove"]
- 3 console.log(Object.getOwnPropertyNames(Object.prototype)) 4 // ["toSource", "toString", "toLocaleString", "valueOf", "watch", "unwatch", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "__defineGetter__", "__defineSetter__", "__lookupGetter__", "__lookupSetter__", "__proto__", "constructor"]
我相信,大家看到这里,对 prototype 还是似懂非懂的。这很正常,毕竟是 js 中比较重要又比较抽象的概念, 不可能那么快就掌握,再啃多几篇,说不定掌握其精髓。在某乎上,有一个活生生的实例,可能也是大家会遇到的问题。可以看看 。
5.constructor 属性
对象有一个
- prototype
属性,默认指向
- constructor
对象所在的构造函数。
- prototype
- 1
- function A() {};
- 2 console.log(A.prototype.constructor === A) //true
要注意的是,prototype 是构造函数的属性,而 constructor 则是构造函数的 prototype 属性所指向的那个对象,也就是原型对象的属性。注意不要混淆。
- 1 console.log(A.hasOwnProperty('prototype')); //true
- 2 console.log(A.prototype.hasOwnProperty('constructor')); //true
由于
属性是定义在原型(
- constructor
对象上面,意味着可以被所有实例对象继承。
- prototype)
- 1
- function A() {};
- 2
- var a = new A();
- 3 4 console.log(a.constructor); //A()
- 5 console.log(a.constructor === A.prototype.constructor); //true
上面代码中,
是构造函数
- a
的实例对象,但是
- A
自身没有
- a
属性,该属性其实是读取原型链上面的 A
- contructor
属性。
- .prototype.constructor
5.1:constructor 属性的作用
a:分辨原型对象到底属于哪个构造函数
- 1
- function A() {};
- 2
- var a = new A();
- 3 4 console.log(a.constructor === A) //true
- 5 console.log(a.constructor === Array) //false
上面代码表示,使用
属性,确定实例对象
- constructor
的构造函数是
- a
,而不是
- A
。
- Array
b:从实例新建另一个实例
- 1
- function A() {};
- 2
- var a = new A();
- 3
- var b = new a.constructor();
- 4 console.log(b instanceof A); //true
上面代码中,
是构造函数
- a
的实例对象,可以从 a
- A
间接调用构造函数。
- .constructor
c:调用自身的构造函数成为可能
- 1 A.prototype.hello = function() {
- 2
- return new this.constructor();
- 3
- }
d:提供了一种从构造函数继承另外一种构造函数的模式
- 1
- function Father() {}
- 2 3
- function Son() {
- 4 Son.height.constructor.call(this);
- 5
- }
- 6 7 Son.height = new Father();
上面代码中,
和
- Father
都是构造函数,在
- Son
内部的
- Son
上调用
- this
,就会形成
- Father
继承
- Son
的效果。
- Father
e:由于 constructor 属性是一种原型对象和构造函数的关系,所以在修改原型对象的时候,一定要注意 constructor 的指向问题。
解决方法有两种,要么将 constructor 属性指向原来的构造函数,要么只在原型对象上添加属性和方法,避免 instanceof 失真。
6.instanceof 运算符
运算符返回一个布尔值,表示指定对象是否为某个构造函数的实例。
- instanceof
- 1
- function A() {};
- 2
- var a = new A();
- 3 console.log(a instanceof A); //true
因为 instanceof 对整个原型链上的对象都有效,所以同一个实例对象,可能会对多个构造函数都返回 true。
- 1
- function A() {};
- 2
- var a = new A();
- 3 console.log(a instanceof A); //true
- 4 console.log(a instanceof Object); //true
注意,instanceof 对象只能用于复杂数据类型(数组,对象等),不能用于简单数据类型(布尔值,数字,字符串等)。
- 1
- var x = [1];
- 2
- var o = {};
- 3
- var b = true;
- 4
- var c = 'string';
- 5 console.log(x instanceof Array); //true
- 6 console.log(o instanceof Object); //true
- 7 console.log(b instanceof Boolean); //false
- 8 console.log(c instanceof String); //false
此外,null 和 undefined 都不是对象,所以 instanceof 总是返回 false。
- 1 console.log(null instanceof Object); //false
- 2 console.log(undefined instanceof Object); //false
利用
运算符,还可以巧妙地解决,调用构造函数时,忘了加
- instanceof
命令的问题。
- new
- 1
- function Keith(name, height) {
- 2
- if (!this instanceof Keith) {
- 3
- return new Keith(name, height);
- 4
- }
- 5 this.name = name;
- 6 this.height = height;
- 7
- }
上面代码中,使用了 instanceof 运算符来判断函数体内的 this 关键字是否指向构造函数 Keith 的实例,如果不是,就表明忘记加 new 命令,此时构造函数会返回一个对象实例,避免出现意想不到的结果。
因为限于篇幅的原因,暂时介绍到这里。
我会在下次的分享中谈谈原型(prototype)对象的一些原生方法,如 Object.getPrototypeOf(),Object.setPrototypeOf() 等,并且介绍获取原生对象方法的比较。
完。
感谢大家的阅读。
来源: http://www.cnblogs.com/Uncle-Keith/p/5834289.html