这阵子因为要学习React Native ,而RN必备的知识就是JS,所以开始学习起来了Js,在此声明写博客就是为了更好的学习,总结一些我学习过程中知识点,有不同意见的请尽情发表!
1 JavaScript对象的创建方式
在JavaScript中,创建对象的方式包括两种:对象字面量和使用new表达式。对象字面量是一种灵活方便的书写方式,例如:
1
2
3
4
5
6
|
var o1 = {
p:”I’m in Object literal”,
alertP:function(){
alert(
this
.p);
}
}
|
这样,就用对象字面量创建了一个对象o1,它具有一个成员变量p以及一个成员方法alertP。这种写法不需要定义构造函数,因此不在本文的讨论范围之内。这种写法的缺点是,每创建一个新的对象都需要写出完整的定义语句,不便于创建大量相同类型的对象,不利于使用继承等高级特性。
new表达式是配合构造函数使用的,例如new String(“a string”),调用内置的String函数构造了一个字符串对象。下面我们用构造函数的方式来重新创建一个实现同样功能的对象,首先是定义构造函数,然后是调用new表达式:
1
2
3
4
5
6
7
|
function CO(){
this
.p = “I’m in constructed object”;
this
.alertP = function(){
alert(
this
.p);
}
}
var o2 = newCO();
|
那么,在使用new操作符来调用一个构造函数的时候,发生了什么呢?其实很简单,就发生了四件事:
1
2
3
4
|
var obj ={};
obj.__proto__ = CO.prototype;
CO.call(obj);
return
obj;
|
第一行,创建一个空对象obj。
第二行,将这个空对象的__proto__成员指向了构造函数对象的prototype成员对象,这是最关键的一步,具体细节将在下文描述。
第三行,将构造函数的作用域赋给新对象,因此CO函数中的this指向新对象obj,然后再调用CO函数。于是我们就给obj对象赋值了一个成员变量p,这个成员变量的值是” I’min constructed object”。
第四行,返回新对象obj。当构造函数里包含返回语句时情况比较特殊,这种情况会在下文中说到。
2 正确定义JavaScript构造函数
不同于其它的主流编程语言,JavaScript的构造函数并不是作为类的一个特定方法存在的;当任意一个普通函数用于创建一类对象时,它就被称作构造函数,或构造器。一个函数要作为一个真正意义上的构造函数,需要满足下列条件:
1、 在函数内部对新对象(this)的属性进行设置,通常是添加属性和方法。
2、 构造函数可以包含返回语句(不推荐),但返回值必须是this,或者其它非对象类型的值。
上文定义的构造函数CO就是一个标准的、简单的构造函数。下面例子定义的函数C1返回了一个对象,我们可以使用new表达式来调用它,该表达式可以正确返回一个对象:
1
2
3
4
5
6
7
8
|
function C1(){
var o = {
p:”I’m p in C1”
}
return
o;
}
var o1 =
new
C1();
alert(o1.p);
//I’m p in C1
|
但这种方式并不是值得推荐的方式,因为对象o1的原型是函数C1内部定义的对象o的原型,也就是Object.prototype。这种方式相当于执行了正常new表达式的前三步,而在第四步的时候返回了C1函数的返回值。该方式同样不便于创建大量相同类型的对象,不利于使用继承等高级特性,并且容易造成混乱,应该摒弃。
一个构造函数在某些情况下完全可以作为普通的功能函数来使用,这是JavaScript灵活性的一个体现。下例定义的C2就是一个“多用途”函数:
1
2
3
4
5
6
7
8
9
10
|
function C2(a, b){
this
.p = a + b;
this
.alertP = function(){
alert(
this
.p);
}
return
this
.p;
//此返回语句在C2作为构造函数时没有意义
}
var c2 =
new
C2(
2
,
3
);
c2.alertP();
//结果为5
alert(C2(
2
,
3
));
//结果为5
|
该函数既可以用作构造函数来构造一个对象,也可以作为普通的函数来使用。用作普通函数时,它接收两个参数,并返回两者的相加的结果。为了代码的可读性和可维护性,建议作为构造函数的函数不要掺杂除构造作用以外的代码;同样的,一般的功能函数也不要用作构造对象。
3 为什么要使用构造函数
根据上文的定义,在表面上看来,构造函数似乎只是对一个新创建的对象进行初始化,增加一些成员变量和方法;然而构造函数的作用远不止这些。为了说明使用构造函数的意义,我们先来回顾一下前文提到的例子。执行var o2 = new CO();创建对象的时候,发生了四件事情:
1
2
3
4
|
var obj ={};
obj.__proto__ = CO.prototype;
CO.call(obj);
return
obj;
|
我们说最重要的是第二步,将新生成的对象的__proto__属性赋值为构造函数的prototype属性,使得通过构造函数创建的所有对象可以共享相同的原型。这意味着同一个构造函数创建的所有对象都继承自一个相同的对象,因此它们都是同一个类的对象。关于原型(prototype)和继承的细节,笔者会再另一篇文章中深入说明。
在JavaScript标准中,并没有__prop__这个属性,不过它现在已经是一些主流的JavaScript执行环境默认的一个标准属性,用于指向构造函数的原型。该属性是默认不可见的,而且在各执行环境中实现的细节不尽相同,例如IE浏览器中不存在该属性。我们只要知道JavaScript对象内部存在指向构造函数原型的指针就可以了,这个指针是在调用new表达式的时候自动赋值的,并且我们不应该去修改它。
在构造对象的四个步骤中,我们可以看到,除第二步以外,别的步骤我们无须借助new表达式去实现,因此new表达式不仅仅是对这四个步骤的简化,也是要实现继承的必经之路。
在构造函数中this的引用是构造函数创建的对象,在常规函数中this的引用是调用该方法的对象。
4 容易混淆的地方
关于JavaScript的构造函数,有一个容易混淆的地方,那就是原型的constructor属性。在JavaScript中,每一个函数都有默认的原型对象属性prototype,该对象默认包含了两个成员属性:constructor和__proto__。关于原型的细节就不在本文赘述了,我们现在关心的是这个constructor属性。
按照面向对象的习惯性思维,我们说构造函数相当于“类”的定义,从而可能会认为constructor属性就是该类实际意义上的构造函数,在new表达式创建一个对象的时候,会直接调用constructor来初始化对象,那就大错特错了。new表达式执行的实际过程已经在上文中介绍过了(四个步骤),其中用于初始化对象的是第三步,调用的初始化函数正是“类函数”本身,而不是constructor。如果没有考虑过这个问题,这一点可能不太好理解,那就让我们举个例子来说明一下吧:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
function C3(a, b){
this
.p = a + b;
this
.alertP = function(){
alert(
this
.p);
}
}
//我们定义一个函数来覆盖C3原型中的constructor,试图改变属性p的值
function fake(){
this
.p =
100
;
}
C3.prototype.constructor = fake;
//覆盖C3原型中的constructor
var c3 =
new
C3(
2
,
3
);
c3.alertP();
//结果仍然为5
|
上述代码手动改变了C3原型中的constructor函数,然而却没有对c3对象的创建产生实质的影响,可见在new表达式中,起初始化对象作用的只能是构造函数本身。那么constructor属性的作用是什么呢?一般来说,我们可以使用constructor属性来测试对象的类型:
1
2
|
var myArray = [
1
,
2
,
3
];
(myArray.constructor == Array);
// true
|
这招对于简单的对象是管用的,涉及到继承或者跨窗口等复杂情况时,可能就没那么灵光了:
1
2
3
4
5
6
7
|
function f() {
this
.foo =
1
;}
function s() {
this
.bar =
2
; }
s.prototype =
new
f();
// s继承自f
var son =
new
s();
// 用构造函数s创建一个子类对象
(son.constructor == s);
// false
(son.constructor == f);
// true
|
这样的结果可能跟你的预期不相一致,所以使用constructor属性的时候一定要小心,或者干脆不要用它。
4 返回值
function
Person( name){
this
.name =name;
}
var
p1=
new
Person(
'John'
);
等同于:
function
person(name ){
Object obj =
new
Object();
obj.name =name;
return
obj;
}
var
p1= person(
"John"
);
function
Person( name){
Object.defineProperty(
this
,
"name"
{
get :
function
(){
return
name;
},
set:
function
(newName){
name =newName;
},
enumerable :
true
,
//可枚举,默认为false
configurable:
true
//可配置
});
}
var
p1=
new
Person(
'John'
);
(3)在构造函数中使用原型对象:
//比直接在构造函数中写的效率要高的多
Person.prototype.sayName=
function
(){
console.log(
this
.name);
};
但是如果方法比较多的话,大多人会采用一种更简洁的方法:直接使用一个对象字面形式替换原型对 象,如下:
Person.prototype ={
sayName :
function
(){
console.log(
this
.name);
},
toString :
function
(){
return
"[Person "
+
this
.name+
"]"
;
}
};
这种方式非常流行,因为你不用多次键入Person.prototype,但有一个副作用你一定要注意:
使用字面量形式改写了原型对象改变了构造函数的属性,因此他指向Object而不是Person。这是因为原型对象具有一个constructor属性,这是其他对象实例所没有的。当一个函数被创建时,它的prototype属性也被创建,且该原型对象的constructor属性指向该函数。当使用对象字面量形式改写原型对象时,其constructor属性将被置为泛用对象Object.为了避免这一点,需要在改写原型对象的时候手动重置constructor,如下
Person.prototype ={
constructor :Person,
sayName :function(){
console.log(this.name);
},
toString :function(){
return
"[Person "
+ this.name+
"]"
;
}
};
再次测试:
p1.constructor===Person
true
p1.constructor===Object
false
p1 instanceof Person
true
5 匿名函数
匿名函数就是没有名字的函数,有时候也被称为拉姆达(lamda)函数。形式如下:
// 函数体
}
而用作模仿块级作用域(私有作用域)的匿名函数的语法形式如下:
// 块级作用域
})();
以上代码的意思是:首先定义并立即调用一个匿名函数。将函数声明包含在圆括号中,表示它实际上是一个函数表达式。而紧随其后的另一对圆括号表示立即调用这个函数。
可能刚开始我们感觉这种语法比较难以理解,我们可以先看下下面这段代码:
alert(‘函数’);
};
myFunc();
上面的代码中,首先以函数表达式的方式定义了一个函数,然后立即调用它。在这里定义函数的方式就是先创建一个匿名函数,然后将其赋值给变量myFunc,而在函数名称后加一对圆括号即表示调用函数。那我们如果直接用匿名函数代表变量myFunc,在匿名函数后面添加一对圆括号不就表示直接调用了吗?然而,如果如下这样做就会报错:
// 块级作用域
}();
因为在javascript中,function关键字表示一个函数声明的开始,而函数声明后面不能直接跟圆括号。而函数表达式后面可以跟圆括号,来表示函数调用。在函数声明外面加一对圆括号就可以转换成函数表达式,如下:
// 块级作用域
})();
参考:块级作用域