这是我参与8月更文挑战的第18天,活动详情查看:8月更文挑战
构造函数
使用构造函数的实例:
// 自定义构造函数
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
};
}
// 生成对象实例
var person1 = new Person("Bob", 18);
var person2 = new Person("Mike", 20);
在构造函数中, 不需要使用 new
新建对象, 也不需要返回创建的对象.
在生成对象时, 需要使用 new
关键字, new 关键字的用途:
- 创建一个新对象
- 将函数内部的 this 指向了这个新对象
- 执行构造函数内部的代码
- 将新对象作为返回值
new
关键字的功能演示如下:
// 演示 new 的功能
function Person(name,age) {
// var instance = new Object();
// this = instance;
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
};
// return instance;
}
构造函数和实例对象的关系
• 构造函数是根据具体的事物抽象出来的抽象模板
• 实例对象是根据抽象的构造函数模板得到的具体实例对象
每一个实例对象都通过一个 constructor
属性 (不严谨的说法),指向创建该实例的构造函数, 可以通过 constructor 属性判断实例和构造函数之间的关系, 但 constructor
属性实际上是属于原型对象的 (并且可以更改), 并不是实例对象自己的属性, 因此推荐使用 instanceof
操作符
// 每个对象的 constructor 属性值就是生成这个对象的构造函数
console.log(person1.constructor);
// 判断一个对象的具体对象类型,需要使用 instanceof 进行判断
console.log(person1 instanceof Person);
静态成员和实例成员
• 使用构造函数方法创建对象时,可以给构造函数和创建的实例对象添加属性和方法,这些属性和方法都叫做成员。
• 实例成员:在构造函数内部添加给 this 的成员,属于实例对象的成员,在创建实例对象后必须由对象调用。
• 静态成员:添加给构造函数自身的成员,只能使用构造函数调用,不能使用生成的实例对象调用。
// 自定义构造函数
function Person(name,age) {
// 实例成员 ,通过将来生成的实例对象进行调用的成员
// 创建时,是直接添加给函数内部的 this
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
};
}
// 静态成员 -- 直接给构造函数添加的成员
Person.version = "1.0";
// 调用静态成员,只能通过构造函数进行调用
console.log(Person.version);
// 生成对象实例
var person1 = new Person("Bob",18);
var person2 = new Person("Mike",20);
// 调用实例成员
console.log(person1.name);
prototype 原型对象
任何函数都具有一个 prototype 属性,该属性是一个对象。
// 定义构造函数
function Person(name,age) {
this.name = name;
this.age = age;
}
// 获取 构造函数 的 prototype 属性
console.log(Person.prototype);
可以在原型对象上添加属性和方法。
// 对象内部可以添加一些属性和方法
Person.prototype.type = "human";
Person.prototype.sayHi = function () {
console.log("hello");
};
构造函数的 prototype 原型对象默认都有一个 constructor
属性,指向 prototype 对象所在函数。
通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针__proto__
。
__proto__
属性并不是一个标准的属性,是浏览器自己根据语法自动生成的
实例对象可以直接访问原型对象成员。
p1.__proto__.sayHi();
// 在真正开发的过程中,是不会书写 __proto__ 属性的, 简写为:
p1.sayHi();
构造函数、实例、原型对象三者之间的关系
构造函数的问题
- 浪费内存
例如上面的构造函数中除了 name
和 age
之外, 还有 sayName
这个函数, 但创建实例对象的时候没有用到sayName
这个函数, 会造成内存的浪费.
使用原型对象可以更好的解决构造函数的内存浪费问题:
- JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向构造函数的原型对象。这个原型对象的所有属性和方法,都会被构造函数的实例对象所拥有. 因此,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype 对象上。
// 自定义构造函数
function Person(name,age) {
this.name = name;
this.age = age;
}
// 更优的解决方法,将所有实例共享的属性和方法,都添加给原型对象
Person.prototype.type = "human";
Person.prototype.sayName = function () {
// 方法调用时,哪个对象调用,this 指向的就是谁
console.log(this.name);
};
Person.prototype.sayAge = function () {
// 方法调用时,哪个对象调用,this 指向的就是谁
console.log(this.age);
}
// 生成对象实例
var person1 = new Person("Bob",18);
var person2 = new Person("Mike",20);
// 调用原型对象上公用的属性和方法
person2.sayAge();
原型链
首先提出一个问题: 为什么实例对象可以调用构造函数的 prototype 原型对象的属性和方法?
原型链图示
原型链查找机制
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性:
-
搜索首先从对象实例本身开始
-
如果在实例中找到了具有给定名字的属性,则返回该属性的值
-
如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性
-
如果在原型对象中找到了这个属性,则返回该属性的值
// 自定义构造函数
function Person(name,age) {
this.name = name;
this.age = age;
// this.sayName = function () {
// console.log("hello");
// }
}
//原型对象
// Person.prototype.sayName = function () {
// console.log(this.name);
// };
// 生成对象实例
var person1 = new Person("Bob",18);
// 方法的调用查找
person1.sayName();
如果构造函数中的 sayName
没有被注释掉的话, 输出的结果应该是 "hello"
; 如果构造函数中的部分注释掉了, 但是原型对象中的部分没有被注释掉的话, 输出的结果是"Bob"
. 但如果原型对象中的 sayName
也被注释掉了, 沿原型链继续向上找 Object
和 null
中都没有这个方法, 就会报错.
实例对象读写原型对象成员
读取
-
先在自己身上找,找到即返回
-
自己身上找不到,则沿着原型链向上查找,找到即返回
-
如果一直到原型链的末端还没有找到,则返回 undefined
// 自定义构造函数
function Person(name,age) {
this.name = name;
this.age = age;
// this.sayName = function () {
// console.log("hello");
// }
}
// 将所有实例共享的属性和方法,都添加给原型对象
Person.prototype.type = "human";
// Person.prototype.sayName = function () {
// console.log(this.name);
// };
// 生成对象实例
var person1 = new Person("Bob",18);
var person2 = new Person("Mike",20);
// 方法的调用查找
console.log(person1.city);
console.log(person1.sayName);
person1.sayName();
写入
值类型成员写入(实例对象.值类型成员 = xx):
-
当实例期望重写原型对象中的某个普通数据成员时实际上会把该成员添加到自己身上
-
也就是说该行为实际上会屏蔽掉对原型对象成员的访问
引用类型成员写入(实例对象.引用类型成员 = xx):
- 同上
// 通过实例对象添加新成员,会直接添加给自己,会屏蔽掉对原型对象的访问
person1.sex = "male";
person1.sayAge = function () {
console.log(this.age);
};
修改
简单类型成员修改(实例对象.成员.xx = xx):
- 同上
// 如果通过实例对象更改原型对象的属性和方法,会直接添加给自己,会屏蔽掉对原型对象的访问
person1.type = "person";
person1.sayName = function () {
console.log(this.name);
}
console.dir(person1);
复杂类型成员修改(实例对象.成员.xx = xx):
-
同样会先在自己身上找该成员,如果自己身上找到则直接修改
-
如果自己身上找不到,则沿着原型链继续查找,如果找到则修改
-
如果一直到原型链的末端还没有找到该成员,则报错(实例对象.undefined.xx = xx)
// 添加一个新的属性给原型对象,值是一个对象类型
Person.prototype.address = {
city : "北京"
};
// 通过实例对象更改原型对象中复杂类型数据中的内容,还是会进行原型链的查找
person1.address.city = "上海";
更简单的原型语法
前面在原型对象每添加一个属性和方法就要书写一遍 Person.prototype 。
为减少不必要的输入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象,将 Person.prototype 重置到一个新的对象。
注意:原型对象会丢失 constructor 成员,所以需要手动将 constructor 指向正确的构造函数。
// 直接使用一个对象字面量对 原型对象进行赋值
Person.prototype = {
constructor : Person, // 需要手动 将 constructor 属性指向正确的构造函数
type : "human",
sayName : function () {
console.log(this.name);
}
};
// 生成对象实例
var person1 = new Person("Bob",18);
var person2 = new Person("Mike",20);
person1.sayName();
console.log(person2.constructor);
原型对象使用建议
在定义构造函数时,可以根据成员的功能不同,分别进行设置:
-
私有成员(一般就是非函数成员)放到构造函数中
-
共享成员(一般就是函数)放到原型对象中
-
如果重置了 prototype 记得修正 constructor 的指向
JS 原生构造函数的原型对象
所有函数都有 prototype 属性对象。
JavaScript中的内置构造函数也有 prototype 原型对象属性:
-
Object.prototype
-
Function.prototype
-
Array.prototype
-
String.prototype
-
Number.prototype
-
...
为数组对象扩展原型方法
// 直接给原型对象添加一条新的属性
// 不允许更改内置的原型对象
Array.prototype.getEvenSum = function () {
// 获取数组中每一项的方式
// this[i]
var sum = 0;
for (var i = 0 ; i < this.length ; i++) {
if (i % 2 === 0) {
sum += this[i];
}
}
return sum;
};
// 定义一个数组
var arr = [2,4,5,7,9];
console.log(arr.getEvenSum());
console.dir(Array.prototype);