大多数语言的变量都有作用域这个概念,Javascript也不例外,关于作用域的介绍,在奇怪的JS中有部分介绍。
与大多数语言不同,Javascript没有public
, private
等关键字。那么,如何在Javascript中实现其他语言中的数据封装呢?
全局危险
我们看一个例子:
var name = 'wanwu.tech'
var showName = function() {
console.log(name);
}
showName();
wanwu.tech
可以看到,在函数内部,我们是可以看到外部的变量name
的。这些外部定义可以任何地方使用的变量,就是全局变量。这里我们仅仅是显示了一个全局变量,如果不小心修改了,那就比较头疼了。
不想被偷窥
假设你设计了一个猜谜游戏:
var question = '我的名字是什么';
var answer = "万物";
console.log(question)
你肯定在显示出问题后,并不希望玩家可以看到答案,但是在现在这种设计中,你无法阻止玩家读取答案:
console.log(answer)
你甚至无法阻止玩家修改答案:
answer = '修改了`
更方便的修改代码
如果内部大量使用全局变量,甚至修改全局变量,那么代码重构会成为一个大问题。
命名冲突
写程序多了,大家都发现,找个合适的名字,真的不容易。如果好不容易找到一个合适的名字,却和全局变量冲突,得有多么郁闷。
局部变量
使用局部变量,解决全局变量的问题. 看下面的例子:
var hiddenName = function() {
var site = 'wanwu.tech';
}
console.log(site);
ReferenceError: site is not defined
at evalmachine.<anonymous>:5:13
at ContextifyScript.Script.runInThisContext (vm.js:23:33)
at Object.runInThisContext (vm.js:95:38)
at run ([eval]:617:19)
at onRunRequest ([eval]:388:22)
at onMessage ([eval]:356:17)
at emitTwo (events.js:106:13)
at process.emit (events.js:194:7)
at process.nextTick (internal/child_process.js:766:12)
at _combinedTickCallback (internal/process/next_tick.js:73:7)
可见,在hiddenName()
外部,site
是可不见的,它是一个局部变量。在函数内部声明的变量,作用于局限在此函数内,是局部变量。我们可以使用接口来规范我们对数据的使用。这里所说的接口是你提供给使用者可使用的属性和函数。
接口
不好的接口
首先看一个不好的例子, 用了全局变量设计一个计数器:
var counter = 0
var count = function () {
counter = counter + 1;
console.log(counter)
return counter;
}
// 正常使用
count();
count();
// 非正常使用 -- 但是无法阻止
counter = 100;
count();
1
2
101
101
很明显, 这个程序在非正常使用的时候, counter
可以轻易被修改, 从而影响计数器的工作.
我们可以使用封装的方法, 将变量封装在函数中.
好的接口
我们希望外部不可见counter
, 但是可以通过调用count
来间接修改counter
.
我们一步一步解决问题:
首先, 外部不可见counter
:
function addBy1() {
var counter = 0;
counter = counter + 1;
console.log(counter)
return counter;
}
addBy1();
addBy1();
1
1
1
修改程序后, 虽然外部不可见counter
, 但是我们没有办法记录我们的计算过程了, 也就没有办法记录修改后的计数了, 必须想办法解决.
那么下一步, 可以在函数中内嵌一个函数来解决:
function addBy1() {
var counter = 0;
var count = function() {
counter = counter + 1;
return counter;
}
return count;
}
var countBy1 = addBy1();
countBy1();
countBy1();
countBy1();
3
这个方法的关键就是addBy1()
内嵌了一个count()
函数, 而且它作为addBy1()
的返回值返回, 使得它的生存时间比addBy1()
还要长. 也就是说, 在addBy1()
释放掉内存后, count()
还存在, 而且count()
所需的生存环境也还存在. 这里count()
的生存环境其中之一就是counter
变量.
通过上面方法, 我们使用countBy1()
获取了addBy1()
方法的返回值count
, 那么相当于就是创建了一个小空间, 这个空间包括了具体的执行函数count()
, 也包括count()
所需环境counter
变量. 每次调用countBy1()
, 都是这个函数执行在其环境中, 也就是可以记录counter
的值了. 那么这里的count
函数就是一个闭包.
这是一个JavaScript常见的设计模式: module pattern
这样一来, 我们就设计一个接口addBy1
, 你不需要知道里面发生了什么, 也不能知道里面发生了什么, 只要调用这个接口就可以了.
环境独立
如果我们使用不同的变量指向addBy1
, 这些不同的变量将会有不同的环境, 也就是它们的环境相互独立, 互不影响.
var count1 = addBy1();
var count2 = addBy1();
var count3 = addBy1();
console.log(count1());
console.log(count2());
console.log(count3());
console.log(count1());
console.log(count2());
console.log(count3());
1
1
1
2
2
2
使用构造函数
我们也可以使用构造函数来构造一个对象:
var AddBy1 = function() {
var counter = 0;
this.count = function() {
counter += 1;
return counter;
}
}
var count1 = new AddBy1();
count1.count();
count1.count();
count1.count();
3
比较这个方法和前面的方法, 它们几乎是一样的. 唯一的区别就是这里使用的是构造函数. 注意这里的counter
不是this.counter
, 否则外部就可以访问到了.
设计一个猜谜游戏
封装
假设这个猜谜游戏可以做到:
- 显示问题: quizeMe().
- 显示当前问题答案: showMe().
- 转到下一题: next().
我们还需要一些变量存储问题和答案, 但是我们不希望它们声明在全局, 所以我们可以将它们设置为一个对象的属性:
var quiz = {
questions: [
{question: 'question1', answer: 'answer1'},
{question: 'question2', answer: 'answer2'},
{question: 'question3', answer: 'answer3'},
{question: 'question4', answer: 'answer4'}
],
index: 0,
quizMe: function() {
console.log(this.questions[this.index].question);
},
showMe: function() {
console.log(this.questions[this.index].answer);
},
next: function() {
this.index += 1;
}
}
quiz.quizMe();
quiz.showMe();
quiz.next();
quiz.quizMe();
quiz.showMe();
question1
answer1
question2
answer2
上面的代码完全满足了前面上的三点, 但是还有一个严重的问题: quiz
的所有属性都是外部可见的, 我们甚至可以修改问题和答案:
quiz.questions.quesion[0] = 'ahaha'
这样是不是太随便了?
这个时候, 我们就可以使用module pattern来解决了.
var getQuiz = function() {
// 局部变量, 外部不可以访问, 相当于private
var questions = [
{question: 'question1', answer: 'answer1'},
{question: 'question2', answer: 'answer2'},
{question: 'question3', answer: 'answer3'},
{question: 'question4', answer: 'answer4'}
]
// 局部变量, 外部不可以访问, 相当于private
var index = 0
return {
quizMe: function() {
console.log(questions[index].question);
},
showMe: function() {
console.log(questions[index].answer);
},
next: function() {
index += 1;
}
}
}
quiz = getQuiz();
quiz.quizMe();
quiz.showMe();
quiz.next();
quiz.quizMe();
quiz.showMe();
question1
answer1
question2
answer2
现在, 我们只能使用return
返回的内容, 而不能访问questions
等内部变量.
检查答案
var getQuiz = function() {
// 使用一个 var 声明所有变量
var score = 0,
index = 0,
inPlay = true,
questions,
next,
getQuestion,
checkAnswer,
submit;
questions = [
{question: 'question1', answer: 'answer1'},
{question: 'question2', answer: 'answer2'},
{question: 'question3', answer: 'answer3'},
{question: 'question4', answer: 'answer4'}
]
// 下一题
next = function() {
index += 1;
if (index >= questions.length) {
inPlay = false;
console.log('游戏结束');
}
}
// 返回当前问题
getQuestion = function() {
if (inPlay) {
return questions[index].question;
} else {
return '游戏已经结束';
}
}
// 检查答案
checkAnswer = function(userAnswer) {
// 注意这里判断相等的方法
if (userAnswer === questions[index].answer) {
console.log('正确');
score += 1;
} else {
console.log('错误');
}
}
// 提交答案
submit = function(userAnswer) {
var message = '游戏结束';
if (inPlay) {
checkAnswer(userAnswer);
next();
message = `你得分为: ${score}`;
}
return message;
}
return {
quizMe: getQuestion,
submit: submit
}
}
var quiz = getQuiz();
quiz.quizMe();
quiz.submit('answer1');
Immediately invoked function expressions (IIFE)
为什么使用IIFE
我们勾勒一下上面的例子, 看看我们总体上做了什么:
var getQuiz = function() {
...
return {
quizMe: getQuestion,
submit: submit
}
}
var quiz = getQuiz();
quiz.quizMe()
quiz.submit('answer1')
这里我们着重关注全局变量, 一共两个: getQuiz
和quiz
.
我们只使用了一次getQuiz
, 但是仍然声明了这个全局变量, 这么一下就把全局污染了.
我们可以使用IIFE将全局变量减半
function expression
我们都很熟悉function expression(函数表达式):
var show = function(message) {
console.log(message)
}
var namespace = {
show: function(message) {
console.log(message)
}
}
tweets.forEach(function(message) {
console.log(message)
})
var getFunction = function() {
var localMessage = 'Hello Local'
return function() {
console.log(localMessage)
}
}
然后我们就可以调用这些函数了:
show('Hello)
namespace.show('Hello')
var show = getFunction()
show()
你应该已经注意到, 我们使用括号:()
来调用函数, 这里()
就叫做函数调用操作符. 如果有参数, 那么将参数传入函数调用操作符之内.
写法
IIFE写法有两种, 如下所示:
(function () {
console.log("Hello World!");
})();
(function () {
console.log("Hello World!");
}());
Hello World!
Hello World!
你可以想象全局有东西使用了()
调用了函数, 使其立即执行
使用IIFE优化我们的猜谜游戏
var quiz = (function() {
...
return {
quizMe: getQuestion,
submit: submit
}
})()
quiz.quizMe()
quiz.submit('answer1')
这样, 我们减少了一个全局变量.