首先讲讲为什么会有闭包和原型。
Javascript的最初就是一款脚本语言(名字中带scrip就足以说明这一点),用来开发网页,所以语法简洁,没有面向对象语言的概念(多态,继承,封装。。。),甚至没有线程。
为了实现多态,继承和封装,我们看看js是如何实现这些OOP特性的。
封装的概念非常简单,就是将用户不需要知道的数据和方法隐藏起来,外部无法直接访问。在Java中,可以用private, protected关键字进行修饰。在JS中可以用闭包实现。来看一个例子:
var person = {
fullName : "Jason Shapiro",
};
alert(person.fullName); // Jason Shapiro
person.fullName = "Jim White";
alert(person.fullName); // Jim White
//不小心将邮箱赋给fullName
person.fullName = [email protected];
alert(person.fullName); // [email protected]
这里person对象的fullName属性是暴露给用户的,你无法保证用户总是赋合法的名字。
为了解决这一问题,我们可以用闭包。在此之前,我们先开看看函数的作用域(Function Scope)的概念:
Variables declared in a function are hidden from any code outside of its definition.
意思是,任何在函数体内部声明的变量对外部都是隐藏的。这正是我们需要的特性,于是我们将上述person 对象改造成:
var person = function () {
var fullName = "Jason Shapiro";
var reg = new RegExp(/\d+/);
var theObj = {
setFullName : function (newValue) {
if( reg.test(newValue) ) {
alert("invalid name");
}
else {
fullName = newValue; // Legal! The object has access to "fullName"
}
},
getFullName : function () {
return fullName; // Legal! The object has access to "fullName"
}
}; // End of the Object
};
person.getFullName(); // 无法访问,属于函数内部theObj对象的属性
我们已经比较接近了,但是我们无法访问函数内部的变量,因为函数内部声明的变量只能在函数内部访问。于是我们便引出闭包的概念。
a closure is an inner-scope which has access to all of the variables defined outside of its block, even after those variables would have normally “fallen out of scope.”
闭包是即使跳出函数体,仍然能够访问函数内部,闭包外部的变量。
var person = function () {
var fullName = "Jason Shapiro";
var reg = new RegExp(/\d+/);
return {
setFullName : function (newValue) {
if( reg.test(newValue) ) {
alert("Invalid Name");
}
else {
fullName = newValue;
}
},
getFullName : function () {
return fullName;
}
}; // end of the return
};
var p = person();
alert(p.getFullName()); // Jason Shapiro
p.setFullName( "Jim White" );
alert(p.getFullName()); // Jim White
p.setFullName( 42 ); // Invalid Name; the name is not changed.
p.fullName = 42; // Doesn't affect the private fullName variable.
alert(p.getFullName()); // Jim White is printed again.
需要注意的一点时,内部函数访问的是被创建的内部变量本身,而不是它的拷贝。所以在闭包函数内加入 loop 时要格外注意。另外当然的是,闭包特性也可以用于创建私有函数或方法。
如何实现继承?这是通过JS的另一特性原型Prototype来实现的。
在OOP中,通过类的继承来实现代码的复用,通过实例化一个类可以创建许多对象,在JS中继承是通过原型实现的。
let user = function(name) {
this.name = name;
this.getName = function () {
console.log(this.name);
}
};
//为了避免下面比较name时,对值进行比较,这里故意传入了String对象
let user1 = new user(new String('KK'));
let user2 = new user(new String('KK'));
console.log(user1.name === user2.name); //输出false
console.log(user1.getName === user2.getName); //输出false
在上述代码中,我们通过构造函数user,创建了两个对象。实际上是通过复制 构造函数user的原型对象 来创建user1和user2。原型对象中有个constructor指向了user函数,实际上还是通过这个构造函数来创建的对象。
假如不用原型(更准确地说原型对象中没有用户定义的属性),那么这两个对象就无法共享任何属性,对于这个例子来说,getName的逻辑是一样的,不需要两份getName,所有的user对象其实可以共享这个getName方法。这个逻辑非常像java类中的静态函数,只不过静态函数只能够调用静态变量和静态方法。但是在JS的世界里,可以通过将getName定义在原型中,已达到所有对象共享这个函数。
let user = function(name) {
this.name = name;
};
user.prototype.getName = function () {
console.log(this.name);
}
user.prototype.color = new String('White');
let user1 = new user(new String('KK'));
let user2 = new user(new String('KK'));
console.log(user1.name === user2.name); //输出false
console.log(user1.getName === user2.getName); //输出true
console.log(user1.color === user2.color); //输出true
这里就一目了然了。在原型对象中定义的变量和方法能够被所有多个对象共享。原型的属性被对象共享,但是它不属于对象本身。
user1.hasOwnProperty('name'); //true;
user1.hasOwnProperty('getName'); //false;
这里要注意:原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在所有实例对象上。反之,如果对象的属性被修改,原型的对象中相同的属性并不会修改。
原型链
JS中任何对象都有原型,函数对象有原型(只不过函数不充当构造函数时,原型不起作用),普通的JS object也有原型,原型是一个object,它也有原型,这就构成了一个原型链,直到Object.prototype。Object.prototype的原型是null。
读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype
还是找不到,则返回undefined
。如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overriding)。
Why?
简而言之,JavaScript 允许在函数体内部,引用当前环境的其他变量。
由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this
就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。
参考:
[1] encapsulation-in-javascript