新的一年要做点不同的事
新的一年要有新的气象,就从开始写博客。
javascript的面向对象
javascript是没有类的概念,虽然在es6引入了class,通过class关键字,可以定义类,但es6的class更多的只是一个语法糖,只是为了让对象原型的写法更加清晰、更像面向对象编程的语法。但javascript 依然是依靠原型和原型链实现对象属性的继承。
所以学习javascript的面向对象需要先清楚原型和原型链,以及是如何实现继承的
prototype
《JavaScript高级程序设计》: 的书上对原型的解释是:
我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象, 而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么prototype就是通过调用构造函数而创建的那个对象实例的原型对象。
MDN 上的解释是:
JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性
这么说有点生硬,还是直接上代码,在chrome浏览器上来个经典的Person构造函数:
function Person() {};
console.log(Person.prototype);
// {
// constructor: ƒ Person()
// __proto__: Object
// }
const person1 = new Person();
const person2 = new Person();
console.log(person1);
// {
// __proto__:
// constructor: ƒ Person()
// __proto__: Object
// }
console.log(person2);
// {
// __proto__:
// constructor: ƒ Person()
// __proto__: Object
// }
Person.prototype.name = 'Kevin';
console.log(Person.prototype.name); // kevin
console.log(person1.name); // kevin
console.log(person2.name); // kevin
当创建一个的Person构造函数时,打印Person.prototype。可以看到Person.prototype内部定义了一个对象的,而这个对象有两个属性,constructor和__proto__(这两个属性在接下来会讲解)。
接下来使用 new 创建了两个实例对象person1和person2,我们给Person.prototype设置一个属性为name,设置它的值为kevin,可以看到两个实例对象person1和person2都拥有了name属性,值都为kevin,这就是继承。
此时再打印Person.prototype、person1和person2
console.log(Person.prototype);
// {
// name: "kevin"
// constructor: ƒ Person()
// __proto__: Object
// }
console.log(person1);
// {
// __proto__:
// name: "kevin"
// constructor: ƒ Person()
// __proto__: Object
// }
console.log(person2);
// {
// __proto__:
// name: "kevin"
// constructor: ƒ Person()
// __proto__: Object
// }
可以看到person1和person2实例的__proto__内部都多了一个name属性,可以看到构造函数在创建时,会默认生成一个prototype(原型)属性,当我们在prototype上设置一个name属性时,而实例person1和person2会从原型上继承属性。
总结一下prototype的定义:
- 创建函数时,会默认生成一个prototype(原型)属性(prototype是函数才会有的属性)。
- prototype(原型)属性是一个引用类型值,指向一个对象,这个对象正是调用该构造函数而创建的实例的原型。
- prototype(原型)上包含可以由特定类型的所有实例共享的属性和方法。如例子里person1和person2都共享了原型上的name属性。
constructor
prototype是构造函数指向原型的属性,而constructor就是原型指向构造函数的属性。每个原型都有一个constructor属性指向关联的构造函数。
所以看下面的代码:
function Person() {};
console.log(Person === Person.prototype.constructor); // true;
同样的实例也可以通过constructor指向构造函数:
function Person() {}
const person = new Person();
console.log(person.constructor === Person) // true
但事实上,对象实例本身并没有 constructor 属性,对象实例的 constructor 属性是继承自原型的属性。
console.log(person.constructor === Person.prototype.constructor) // true
__proto__
这是每一个JavaScript对象(除了 null )都具有一个特殊的 [[prototype]] 属性,这个属性会指向该对象的原型,也是实例访问原型上属性和方法。因浏览器的广泛支持,ES6要求全部浏览器(包括IE11)必须部署这个属性为 __proto__。所以在浏览器输入:
function Person() {}
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true
看最初的例子
function Person() {};
console.log(Person.prototype);
// {
// constructor: ƒ Person()
// __proto__: Object
// }
const person1 = new Person();
const person2 = new Person();
console.log(person1);
// {
// __proto__:
// constructor: ƒ Person()
// __proto__: Object
// }
console.log(person2);
// {
// __proto__:
// constructor: ƒ Person()
// __proto__: Object
// }
Person.prototype.name = 'Kevin';
console.log(Person.prototype.name); // kevin
console.log(person1.name); // kevin
console.log(person2.name); // kevin
console.log(Person.prototype);
// {
// name: "kevin"
// constructor: ƒ Person()
// __proto__: Object
// }
console.log(person1);
// {
// __proto__:
// name: "kevin"
// constructor: ƒ Person()
// __proto__: Object
// }
console.log(person2);
// {
// __proto__:
// name: "kevin"
// constructor: ƒ Person()
// __proto__: Object
// }
可以看到实例person1和person2本身并没有name属性,但是它们有一个 __proto__,指向它的原型对象(Person.prototype)。当我们读取person1.name时,person1.name并没有定义,从 person1 对象中找不到 name 属性时就会从 person 的原型也就是 person.__proto__ ,也就是 原型Person.prototype中查找,读取到name属性为kevin,这时候就停止查找,并返回原型上的name属性。这就是原型链的第一个环节。
原型链
如上一个例子,当读取person1的name属性时,最后却返回原型上的name属性。原因是实例上没有找到我们想要的属性,这时候会自动从实例的原型,就是person1.__proto__查找,但是如果还是还是没有找到呢?这时会再向上查找,查找原型的原型,也就是person1.__proto__.__proto__...。这样层层向上搜索,直到找到一个名字匹配的属性时停止并返回对应的属性值,或者直到一个对象的原型对象为 null,根据定义,null 没有原型,并作为这个原型链中的最后一个环节,返回结果undefined。
这种通过__proto__不断关联对象原型的,并继承原型上的方法和属性过程就时原型链。
补充
-
实例跟构造函数是没有直接关系的,实例上没有属性可以指向构造函数,实例继承的是原型上的属性和方法。但可以通过instanceof运算符判断,instanceof的作用是如果变量是指定引用类型(上面例子是Person)的实例,会返回true。对比constructor,instanceof在检测对象类型时,更为可靠。因为constructor是原型上的属性,可以被属性值可以被修改。
-
constructor 有一个小技巧是,可以在 constructor 属性的末尾添加圆括号,可以用这个方法创建新的实例对象。所以new Person()和new person.constructor()实际效果是相同的,都是以Person为模板创建新的实例。
-
__proto__是一个内部属性,而且只在主流浏览器上被定义,因此无论从语义的角度,还是从兼容性的角度,都不要使用这个属性。而是使用Object.setPrototypeOf()、Object.getPrototypeOf()、Object.create()操作实例的原型。还可以通过isPrototypeOf(),判断实例和原型的关系。
参考
Javascript高级程序设计
MDN|对象原型
MDN|继承与原型链
JavaScript深入之从原型到原型链
ECMAScript 6 入门
JavaScript原型与原型链