万物科技 学,以致用

JavaScript 7. 作用域

2017-06-07
Geng

大多数语言的变量都有作用域这个概念,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, 否则外部就可以访问到了.

设计一个猜谜游戏

封装

假设这个猜谜游戏可以做到:

  1. 显示问题: quizeMe().
  2. 显示当前问题答案: showMe().
  3. 转到下一题: 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')

这里我们着重关注全局变量, 一共两个: getQuizquiz.

我们只使用了一次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')

这样, 我们减少了一个全局变量.


Comments

你可以请我喝喝茶,聊聊天,鼓励我

Wechat Pay
wechat

Thanks!