通常,构造器没有 return 语句。它们的任务是将所有必要的东西写入 this,并自动转换为结果。但是,如果这有一个 return 语句,那么规则就简单了:
如果 return 返回的是一个对象,则返回这个对象,而不是 this。如果 return 返回的是一个原始类型,则忽略。
换句话说,带有对象的 return 返回该对象,在所有其他情况下返回 this。
例如,这里 return 通过返回一个对象覆盖 this:
function BigUser() {
this.name="John";
return { name: "Godzilla" }; // <-- 返回这个对象
}
alert( new BigUser().name ); // Godzilla,
这里有一个 return 为空的例子
(或者我们可以在它之后放置一个原始类型,没有什么影响):
function SmallUser() {
this.name="John";
return; // <-- 返回 this
}
alert( new SmallUser().name ); // John
通常构造器没有 return 语句。这里我们主要为了完整性而提及返回对象的特殊行为。
顺便说一下,如果没有参数,我们可以省略 new 后的括号:
let user=new User; // <-- 没有参数
// 等同于 let user=new User();
这里省略括号不被认为是一种“好风格”,但是规范允许使用该语法。
如果构造函数内部有return语句,而且return后面跟着一个对象,
new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象。
var Vehicle=function () {
this.price=1000;
return 1000;
};
(new Vehicle())===1000 // false
上面代码中,构造函数Vehicle的return语句返回一个数值。
这时,new命令就会忽略这个return语句,返回“构造”后的this对象。
但是,如果return语句返回的是一个跟this无关的新对象,new命令会返回这个新对象,而不是this对象。这一点需要特别引起注意。
var Vehicle=function (){
this.price=1000;
return { price: 2000 };
};
(new Vehicle()).price // 2000
上面代码中,构造函数Vehicle的return语句,返回的是一个新对象。new命令会返回这个对象,而不是this对象。
另一方面,如果对普通函数(内部没有this关键字的函数)使用new命令,则会返回一个空对象。
function getMessage() {
return 'this is a message';
}
var msg=new getMessage();
msg // {} typeof msg // "object"
上面代码中,getMessage是一个普通函数,返回一个字符串。对它使用new命令,会得到一个空对象。
这是因为new命令总是返回一个对象,要么是实例对象,要么是return语句指定的对象。本例中,return语句返回的是字符串,所以new命令就忽略了该语句。
使用构造函数来创建对象会带来很大的灵活性。构造函数可能有一些参数,这些参数定义了如何构造对象以及要放入什么。
当然,我们不仅可以将属性添加到 this 中,还可以添加方法。
例如,下面的 new User(name) 用给定的 name 和方法 sayHi 创建了一个对象:
function User(name) {
this.name=name;
this.sayHi=function() {
alert( "My name is: " + this.name
);
};
}
let john=new User("John");
john.sayHi(); // My name is: John /* john={ name: "John", sayHi: function() { ... } } */
案例1:是否可以创建像 new A()==new B() 这样的函数 A 和 B?
function A() { ... }
function B() { ... }
let a=new A;
let b=new B;
alert( a==b ); // true
答案:
是的,这是可以的。
如果一个函数返回一个对象,那么 new 返回那个对象而不是 this。
所以它们可以,例如,返回相同的外部定义的对象 obj:
let obj={};
function A() {
return obj;
}
function B() {
return obj;
}
alert( new A()==new B() ); // true
new 操作符执行时,它按照以下步骤:
一个新的空对象被创建并分配给 this。函数体执行。通常它会修改 this,为其添加新的属性。返回 this 的值。
function User(name) {
// this={};(隐式创建)
// 添加属性到this
this.name=name;
this.isAdmin=false;
// return this;(隐式返回) }
如果我们想创建其他用户,我们可以调用 new User("Ann"),new User("Alice") 等。
比每次都使用字面量创建要短得多,而且更易于阅读。
这是构造器的主要目的 — 实现可重用的对象创建代码。
new命令的作用就是执行构造函数,返回一个对象实例。
var Vehicle=function () {
this.price=1000;
};
var v=new Vehicle();
v.price // 1000
上面代码通过new命令,让构造函数Vehicle生成一个实例对象,保存在变量v中。
这个新生成的实例对象,从构造函数Vehicle得到了price属性。
new命令执行时,构造函数内部的this,就代表了新生成的实例对象,
this.price表示实例对象有一个price属性,值是1000。
一个很自然的问题是,如果忘了使用new命令,直接调用构造函数会发生什么事?
这种情况下,构造函数就变成了普通函数,并不会生成实例对象。
而且由于后面会说到的原因,this这时代表全局对象,将造成一些意想不到的结果。
var Vehicle=function (){
this.price=1000;
};
var v=Vehicle();
v // undefined
price // 1000
上面代码中,调用Vehicle构造函数时,忘了加上new命令。结果,变量v变成了undefined,
而price属性变成了全局变量。
因此,应该非常小心,避免不使用new命令、直接调用构造函数。
为了保证构造函数必须与new命令一起使用,
一个解决办法是,构造函数内部使用严格模式,即第一行加上use strict。
这样的话,一旦忘了使用new命令,直接调用构造函数就会报错。
function Fubar(foo, bar){
'use strict';
this._foo=foo;
this._bar=bar;
}
Fubar() // TypeError: Cannot set property '_foo' of undefined
上面代码的Fubar为构造函数,use strict命令保证了该函数在严格模式下运行。
由于严格模式中,函数内部的this不能指向全局对象,默认等于undefined,导致不加new调用会报错(JavaScript 不允许对undefined添加属性)。
另一个解决办法,构造函数内部判断是否使用new命令,如果发现没有使用,则直接返回一个实例对象。
function Fubar(foo, bar) {
if (!(this instanceof Fubar)) {
return new Fubar(foo, bar);
}
this._foo=foo;
this._bar=bar;
}
Fubar(1, 2)._foo // 1
(new Fubar(1, 2))._foo // 1
上面代码中的构造函数,不管加不加new命令,都会得到同样的结果。
使用new命令时,它后面的函数依次执行下面的步骤。
创建一个空对象,作为将要返回的对象实例。将这个空对象的原型,指向构造函数的prototype属性。将这个空对象赋值给函数内部的this关键字。开始执行构造函数内部的代码。
也就是说,构造函数内部,this指的是一个新生成的空对象,所有针对this的操作,
都会发生在这个空对象上.
构造函数之所以叫“构造函数”,就是说这个函数的目的,就是操作一个空对象(即this对象),将其“构造”为需要的样子。
new命令简化的内部流程,可以用下面的代码表示。
function _new(/* 构造函数 */ constructor, /* 构造函数参数 */ params) {
// 将 arguments 对象转为数组
var args=[].slice.call(arguments);
// 取出构造函数
var constructor=args.shift();
// 创建一个空对象,继承构造函数的 prototype 属性
var context=Object.create(constructor.prototype);
// 执行构造函数
var result=constructor.apply(context, args);
// 如果返回结果是对象,就直接返回,否则返回 context 对象
return (typeof result==='object' && result !=null) ? result : context; }
// 实例 var actor=_new(Person, '张三', 28);
在一个函数内部,我们可以使用new.target 属性来检查它是否被使用 new 进行调用了。
对于常规调用,它为空,对于使用 new 的调用,则等于该函数:
function User() {
alert(new.target);
}
// 不带 "new": User();
// undefined
// 带 "new": new User();
// function User { ... }
它可以被用在函数内部,来判断该函数是被通过 new 调用的“构造器模式”,
还是没被通过 new 调用的是“常规模式”。
我们也可以让 new 调用和常规调用做相同的工作,像这样:
function User(name) {
if (!new.target) {
// 如果你没有通过 new 运行我
return new User(name);
// ……我会给你添加 new
}
this.name=name;
}
let john=User("John"); // 将调用重定向到新用户
alert(john.name); // John
这种方法有时被用在库中以使语法更加灵活。这样人们在调用函数时,无论是否使用了 new,程序都能工作。
不过,到处都使用它并不是一件好事,因为省略了 new 使得很难观察到代码中正在发生什么。而通过 new 我们都可以知道这创建了一个新对象。
函数内部可以使用new.target属性。如果当前函数是new命令调用,new.target指向当前函数,否则为undefined。
function f() {
console.log(new.target===f);
}
f() // false
new f() // true
使用这个属性,可以判断函数调用的时候,是否使用new命令。
function f() {
if (!new.target) {
throw new Error('请使用 new 命令调用!');
}
// ...
}
f() // Uncaught Error: 请使用 new 命令调用!
上面代码中,构造函数f调用时,没有使用new命令,就抛出一个错误。
9:Object.create() 创建实例对象
构造函数作为模板,可以生成实例对象。但是,有时拿不到构造函数,只能拿到一个现有的对象。
我们希望以这个现有的对象作为模板,生成新的实例对象,这时就可以使用Object.create()方法。
var person1={
name: '张三',
age: 38,
greeting: function() {
console.log('Hi! I\'m ' + this.name + '.');
}
};
var person2=Object.create(person1);
person2.name // 张三
person2.greeting() // Hi! I'm 张三.
上面代码中,对象person1是person2的模板,后者继承了前者的属性和方法。