原生构造函数
例子:
//构造函数: Object
//实例
var obj = new Object();
obj.name = "Tom";
obj.age = 20;
注意:
1、下面字面量的写法与上面例子new Object()相同。
var obj = {};
obj.name = "Tom";
obj.age = 20;
2、构造函数本身只是函数,如果要实现由构造函数到创建对象的转变,必须使用new运算符将构造函数进行实例化操作。以这种方式调用构造函数实际上会经历以下4个步骤:
(1)创建一个新对象;
(2)将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
(3)执行构造函数中的代码(为这个新对象添加属性);
(4)返回新对象。
自定义构造函数
例子:
//构造函数
function Obj(name, age) {
this.name = name;
this.age = age;
this.getName = function() {
console.log(this.name)
}
}
//实例
var p1 = new Obj("Tom", 20)
var p2 = new Obj("Amy", 18)
注意:
按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。这种做法主要是为了区分构造函数与其他函数。因为构造函数本身也是函数,只不过可以用来创建对象而已。
重点解析
在前面的例子中,p1 和 p2 分别保存着 Obj 的一个不同实例属性的副本,因此 p1 和 p2 中的方法 getName 虽然同名,但并不相等。
测试代码:
console.log(p1.getName === p2.getName)
//输出:false
如上,创建两个完成同样任务的方法实例确实没有必要,因此,可以像下面这样,把函数转移到构造函数外部,定义全局函数。
例子:
//构造函数
function Obj(name, age) {
this.name = name;
this.age = age;
this.getName = getName;
}
//全局函数
function getName() {
console.log(this.name)
}
//实例
var p1 = new Obj("Tom", 20)
var p2 = new Obj("Amy", 18)
//测试
p1.getName() //输出:Tom
p2.getName() //输出:Amy
console.log(p1.getName === p2.getName) //输出:true
如上,如果对象需要定义很多方法,那么就要定义很多个全局函数,那构造函数就失去了封装属性和方法的意义。每个函数都有一个 prototype 属性,这个属性指向当前函数的原型对象(接下来的课程会详细讲解)。我们可以把所有实例对象共享的属性和方法添加到函数的原型对象上,这样每个实例对象都将共享着对同一个属性和方法的引用。
例子:
//构造函数
function Obj(name, age) {
this.name = name;
this.age = age;
}
//在当前函数的原型对象上添加实例对象中共享的属性和方法
Obj.prototype.job = "It";
Obj.prototype.getName = function() {
console.log(this.name)
}
//实例
var p1 = new Obj("Tom", 20)
var p2 = new Obj("Amy", 18)
//测试
p1.getName() //输出:Tom
p2.getName() //输出:Amy
console.log(p1.job) //输出:It
console.log(p2.job) //输出:It
console.log(p1.job === p2.job) //输出:true
console.log(p1.getName === p2.getName) //输出:true
综上所述,创建对象的最佳方式,就是组合使用构造函数和原型对象:构造函数用于定义直接绑定在实例对象上的属性,而原型对象用于定义所有实例对象共享的方法和属性。这样,每个实例对象都会有自己的一份实例属性的副本,但同时又共享着原型对象上的方法和属性,最大限度地节省了内存。为了更进一步的优化,可以把写在原型对象上的方法和属性封装在构造函数中,然后通过检查某个方法是否有效,来决定是否需要初始化原型。
例子:
//构造函数
function Obj(name, age) {
this.name = name;
this.age = age;
if (typeof(this.getName) != "function") {
Obj.prototype.getName = function() {
console.log(this.name)
}
}
}
//实例
var p1 = new Obj("Tom", 20)
var p2 = new Obj("Amy", 18)
//测试
p1.getName() //输出:Tom
p2.getName() //输出:Amy
console.log(p1.getName == p2.getName) //输出:true
借用构造函数
为了更进一步优化构造函数,我们可以使用一种叫做借用构造函数的技术。借用构造函数,顾名思义,即在当前构造函数的内部调用另一个构造函数。通过使用call或apply改变函数内部的this指向是实现借用构造函数的关键。
例子:
function Father(name, age) {
this.name = name;
this.age = age;
}
function Child() {
//借用 Father
Father.call(this, "Tom", 20);
this.job = "It";
this.getName = function() {
return this.name;
}
}
var p = new Child();
console.log(p.name); //输出:Tom
console.log(p.age); //输出:20
console.log(p.job); //输出:It
console.log(p.getName()); //输出:Tom
*上面的例子中,当我们调用构造函数 Child 创建新实例的时候,就会执行 Father 函数中定义的所有对象初始化代码,并将this指向 p。
New.target
函数大致有两种功能:第一,结合new关键字调用函数时,会创建一个通常称作实例的新对象,并将this绑定到实例对象上;第二,不通过new关键字调用函数,会直接执行代码中的函数体。
在判断函数是否通过new关键字调用的问题上,ES5中最流行的方式是使用instanceof。
例子:
function Obj(name, age) {
if (this instanceof Obj) {
this.name = name;
this.age = age;
console.log("函数使用new调用!");
} else {
console.log("函数没有使用new调用!");
}
}
const p1 = new Obj("Tom", 19); //输出:函数使用new调用!
const p2 = Obj("Tom", 19); //输出:函数没有使用new调用!
ES6引入了new.target对象。当通过new关键字调用函数时,new.target被赋值为new操作符的目标函数;如果没有通过new关键字调用函数,则new.target的值为undefined。
例子:
function Obj(name, age) {
if (new.target === Obj) {
this.name = name;
this.age = age;
console.log("使用new调用Obj函数!");
} else {
console.log("没有使用New调用或调用的不是Obj函数!");
}
}
function Person(name, age) {
Obj.call(this, name, age);
}
const p1 = new Obj("Tom", 19); //输出:使用new调用Obj函数!
const p2 = Obj("Tom", 19); //输出:没有使用New调用或调用的不是Obj函数!
const p3 = new Person("Amy", 19); //输出:没有使用New调用或调用的不是Obj函数!
*new.target属于函数内部绑定的对象,因此不能在函数外使用。箭头函数内部没有new.target的绑定。
文中的代码部分,带有“例子”和“测试代码”字样的,只是用来学习或测试某一功能用的代码,不可以直接用于项目的开发中。带有“代码如下”字样的,都是经过本人测试,简单修改即可用于项目开发中的代码,如有错误,欢迎指出。