<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="../unitl/test.js"></script>
<style>
#results li.pass {color:green;}
#results li.fail {color:red;}
</style>
</head>
<body>
<ul id="results"></ul>
</body>
<script>
/*构造函数创建一个对象,并在该对象也就是函数上下文上添加一个属性skulk。
这个skulk方法再次返回函数上下文,从而
能让我们在函数外部检测函数上下文*/
function Ninja() {
this.skulk = function () {
return this;
}
}
var ninja1 = new Ninja();
var ninja2 = new Ninja();
//检测已创建对象中的skulk方法。每个方法都应该返回滋生已创建的对象。
assert(ninja1.skulk() === ninja1,"The 1st ninja is skulking");
assert(ninja2.skulk() === ninja2,"The 2nd ninja is skulking");
</script>
</html>
这个例子中,我们创建了一个名为Ninja的函数作为构造函数,当通过new关键字调用时会创建一个空对象实例,并将其作为函数上下文(this参数)传递给参数。构造函数中在该对象上创建了一个名为shulk的属性并赋值为一个函数,是的该函数成为新创建对象的一个方法。
一般来讲,会调用构造函数时会发生一系列特殊的操作,如下图所示:使用构建字new调用函数会触发以下几个动作。
- 1.创建一个新的空对象。
- 2.该对象作为this参数传递给构造函数,从而成为构造函数的函数上下文。
- 3.新构造的对象作为new运算符的返回值。
通过两次调用定义的构造函数,我们创建了两个新的Ninja对象。值得注意的是,调用的返回结果存储在变量中,后续通过这些变量应用新创建的Ninja对象。
var ninja1 = new Ninja();
var ninja2 = new Ninja();
然后运行测试代码,确保没吃调用该方法都对预期的对象进行操作:
assert(ninja1.skulk()===ninja1,"The 1st ninja is skulking");
assert(ninja2.skulk() ===ninja2,"The 2nd ninja is skulking");
结果完全正确!现在知道通过构造函数创建和初始化一个新的对象了。通过关键字new 调用函数将返回新创建的对象。但接下来确认是否始终如此。
构造函数返回值
构造函数的目的是初始化新创建的对象,并且新构造的对象会作为构造函数的调用结果(通过new运算符)返回。但当构造函数自身有返回值时会是什么结果?通过下面例子探讨这种情况。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="../unitl/test.js"></script>
<style>
#results li.pass {color:green;}
#results li.fail {color:red;}
</style>
</head>
<body>
<ul id="results"></ul>
</body>
<script>
//定义一个叫做Ninja的构造函数
function Ninja() {
this.skulk = function() {
return true;
}
//构造函数返回一个确认的原始类型值,即数字1
return 1;
}
//该函数以函数的形式被调用,正如预期,期返回值为数字1
assert(Ninja() ===1,"Return value honored when not called as a constructor");
//该函数通过new 关键字以构造函数的形式被调用
var ninja = new Ninja();
//通过测试表明,返回值1被忽略了,一个新的被初始化对象被通过new关键字所返回。
assert(typeof ninja === "object","Object returned when called as a constructor");
assert(typeof ninja.skulk==="function","ninja object has a skulk method");
</script>
</html>
如果执行这段代码,会发现一切正常。事实上,这个Ninja函数虽然返回简单的数字1,但对代码的行为没有显著的影响。如果将Ninja作为一个函数调用,的确会返回1,但如果通过new 关键字将其构造函数调用,会构造并返回一个新的ninja对象。截至目前,一切正常。
但如果尝试做一些改变,一个构造函数返回另一个对象。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="../unitl/test.js"></script>
<style>
#results li.pass {color:green;}
#results li.fail {color:red;}
</style>
</head>
<body>
<ul id="results"></ul>
</body>
<script>
//创建一个全局对象,该对象的rules属性设置为false
var puppet = {
rules:false
}
//尽管初始化了传入的this对象,返回该全局对象。
function Emperor() {
this.rules = true;
return puppet;
}
//作为构造函数调用该函数
var emperor = new Emperor();
//测试表明,变量emperor的值为由构造函数返回的对象,而不是new表达式所返回的对象。
assert(emperor===puppet,"The emperoris merely a puppet!");
assert(emperor.rules === false,"The puppet does not know how how to rule!");
</script>
</html>
这个示例中采用的方式略有不同。首先创建了一个全局对象,通过puppet引用它,并将其包含的rules属性设置为false:
var puppet = {
rules:false;
}
然后定义了一个Emperor函数,她会为新构造的对象添加一个rules属性并设置为true。注意,Emperor函数还有一个特殊点,它返回了全局的puppet对象:
function Emperor() {
this.rules = true;
return puppet;
}
之后通过关键字new将Emperor作为构造函数调用:
var emperor = new Empoeror();
这里设置了一种模棱两可 的情况,新生成的对象会传递给构造函数作为函数上下文this,同时被初始化。但我们显示的返回一个完全不同的puppet对象时,哪个对象最终作为构造函数的返回值呢?
assert(emperor===puppet,"The emperoris merely a puppet!");
assert(emperor.rules === false,"The puppet does not know how how to rule!");
测试结果表明,puppet对象最终成为构造函数调用的返回值,而且在构造函数中对函数上下文的操作都是无效的。最终都会返回puppet。
测试总结:
- 如果构造函数返回一个对象,则该对象将作为整个表达式的值返回,而传入构造函数的this将被丢弃。
- 但是,如果构造函数发挥的是非对象类型,则忽略返回值,返回新创建的对象。
正是由于这些特性,构造含糊的写法一般不用于其他函数。接下来进行更详细的探讨。
编写构造函数的注意事项
构造函数的目的是根据初始条件对函数调用创建的新对象进行初始化。虽然这些函数也可以被"正常"使用,或者被赋值的对象属性从而作为方法调用,但这样并没有太大的意义。例如:
function Ninja() {
this.skulk = function() {
return this;
}
}
var whatever = Ninja();
我们可以将Ninja作为一个简单函数调用,如果在非严格模式下调用的话,skulk属性将创建在window对象上=== 这并非一个十分有效的操作。严格模式下情况会更糟,因为在严格模式下this为定义,因此javascript应用将会崩溃。但
这是好事情,如果在非严格模式下犯这样的错误,很可能被忽略(除非有很好的测试),但在严格模式下则暴露无疑。这也是推荐使用严格模式的一个很好示例。
因为构造函数通常以不同普通函数的方式编码和使用,并且只有作为构造函数调用时才有意义。因此出现了命名约定来区分构造函数和普通的函数及方法。
函数和方法命名通常一描述其行为(skulk,creep,sneak,doSomethingWonderful等)的动词开头,且第一个字母小写。而构造函数则通常以描述所构造对象的名词命名,并以大写字母开头:Ninja,Samurai,Emperor,Ronin等。
很显然,通过构造函数我们可以更优雅的创建多个遵循相同模式的对象,而无需一次次重复相同的代码。通用代码只需要作为构造函数的主体写一次即可。