JavaScript 中的面向对象编程

介绍

JavaScript
是一个强大的面向对象编程语言,但是,并不像传统的编程语言,它采用一个以原型为基础的OOP模型,致使它的语法让大多数开发人员看不懂。另外,JavaScript
也把函数作为首要的对象,这可能会给不够熟悉这门语言的开发人员造成更大的困惑。那就是我们决定放在前面作为一个简短前言进行介绍的原因,并且在
JavaScript 里也可以用作面向对象编程的一个参考。

这个文档没有提供一个面向对象编程的规则预览,但有它们的接口概述。

时间: 2019-06-03阅读: 257标签: 函数闭包函数

命名空间

随着越来越多的第三方库,框架和web依赖的出现,JavaScript发展中的命名空间是势在必行的,我们得尽量避免在全局命名空间的对象和变量的冲突。

不幸的是,JavaScript没有提供支持命名空间的编译,但是我们可以使用对象来得到同样结果。在JavaScript中我们有许多种模式来实现命名空间接口,但是我们覆盖嵌套的命名空间,它在该领域是最常用的模式。

什么是闭包函数?

嵌套命名空间

嵌套的命名空间模式使用对象字面量来捆绑一个特定应用的特定名字的功能。

我们最初创建一个全局对象,并且赋值给一个称为MyApp的变量。

// global namespace
var MyApp = MyApp || {};

上述的语法会检查MyApp是否已经被定义过。假如它已经被定义过,我们简单地把它赋值给自己,但是,我们创建一个空的容器来装载我们的函数和变量。

我们也可以使用相同技术来创建子命名空间。例如:

// sub namespaces
MyApp.users = MyApp.user || {};

我们一旦启动我们的容器,我们可以在(容器)内部定义我们的函数和变量,并且在全局命名空间调用它们,不需要冒着与现有定义冲突的风险。

// declarations

MyApp.users = {

    existingUsers: '', // variable in namespace

    renderUsersHTML: function() { // function in namespace

        // render html list of users

    }

};

// syntax for using functions within our namespace from the global scope

MyApp.users.renderUsersHTML();

在JavaScript命名模式的一个内部概述是由Goggle的Addy Osmani在Essential
JavaScript Namespacing
Patterns的文章中介绍的。假如你想探索不同的模式,这里将是一个美好的起点。

闭包函数是一种函数的使用方式,最常见的如下:

对象

如果你写过 JavaScript 代码,那你已经使用过对象了。JavaScript
有三种类型的对象:

function fn1(){ function fn(){ } return fn;}

原生对象

原生对象是语言规范的一部分,不管在什么样的运行环境下运行,原生对象都可用。原生对象包括:Array、Date、Math
和 parseInt 等。想了解所有原生对象,请参阅 JavaScript
内建对象参考

var cars = Array(); // Array is a native object

这种函数的嵌套方式就是闭包函数,这种模式的好处是可以让内层函数访问到外层函数的变量,并且让函数整体不至于因为函数的执行完毕而被销毁。

宿主对象

与原生对象不同,宿主对象是由 JavaScript
代码运行的环境创建。不同的环境环境创建有不同的宿主对象。这些宿主对象在多数情况下都允许我们与之交互。如果我们写的是在浏览器(这是其中一种运行环境)上运行的代码,会有
window、document、location 和 history 等宿主对象。

document.body.innerHTML = 'Hello World!'; // document is a host object

// the document object will not be available in a 
// stand-alone environments such as Node.js

例如:

用户对象

用户对象(或植入对象)是在我们的代码中定义的对象,在运行的过程中创建。JavaScript
中有两种方式创建自己的对象,下面详述。

function fn1(){ var a =10; function fn(){ console.log(a); // 10 } return fn;}

对象字面量

在前面演示创建命名空间的时候,我们已经接触到了对象字面量。现在来搞清楚对象字面量的定义:对象字面量是置于一对花括号中的,由逗号分隔的名-值对列表。对象字面量可拥有变量(属性)和函数(方法)。像
JavaScript 中的其它对象一样,它也可以作为函数的参数,或者返回值。

现在定义一个对象字面量并赋予一个变量:

// declaring an object literal

var dog = {

    // object literal definition comes here...

};

向这个对象字面量添加属性和方法,然后在全局作用域访问:

// declaring an object literal

var dog = {

    breed: 'Bulldog', // object literal property

    bark: function() { // object literal method

        console.log("Woof!");

    },

};

// using the object

console.log( dog.breed ); // output Bulldog

dog.bark(); // output Woof!

这看起来和前面的命名空间很像,但这并不是巧合。字面量对象最典型的用法就是把代码封装起来,使之在一个封装的包中,以避免与全局作用域中的变量或对象发生冲突。由于类似的原因,它也常常用于向插件或对象传递配置参数。

如果你熟悉设计模式的话,对象字面量在某种程度上来说就是单例,就是那种只有一个实例的模式。对象字面量先天不具备实例化和继承的能力,我们接下来还得了解
JavaScript 中另一种创建自定义对象的方法。

再比如下面的代码,随着函数的每次执行,变量的值都会进行递增1,原因是因为外层函数的变量处于内层函数的作用域链当中,被内层函数所使用着,当js垃圾回收机制读取到这一情况后就不会进行垃圾回收。

构造函数

例如:

定义构造函数

函数是 JavaScript 一等公民,就是说其它实体支持的操作函数都支持。在
JavaScript
的世界,函数可以在运行时进行动态构造,可以作为参数,也可以作为其它函数的返回值,也可被赋予变量。而且,函数也可以拥有自己的属性和方法。JavaScript
中函数的特性使之成为可以实体化和继承的东西。

来看看怎么用构造函数创建一个自定义的对象:

// creating a function

function Person( name, email ) {

    // declaring properties and methods using the (this) keyword

    this.name   = name;
    this.email  = email;

    this.sayHey = function() {

        console.log( "Hey, I’m " + this.name );

    };

}

// instantiating an object using the (new) keyword

var steve = new Person( "Steve", "steve@hotmail.com" );

// accessing methods and properties

steve.sayHey();

创建构造函数类似于创建普通函数,只有一点例外:用 this 关键字定义自发性和方法。一旦函数被创建,就可以用 new 关键字来生成实例并赋予变量。每次使用 new 关键字,this 都指向一个新的实例。

构建函数实例化和传统面向对象编程语言中的通过类实例化并非完全不同,但是,这里存在一个可能不易被察觉的问题。

当使用 new 关键字创建新对象的时候,函数块会被反复执行,这使得每次运行都会产生新的匿名函数来定义方法。这就像创建新的对象一样,会导致程序消耗更多内存。这个问题在现代浏览器上运行的程序中并不显眼。但随着应用规则地扩大,在旧一点的浏览器、计算机或者低电耗设备中就会出现性能问题。不过不用担心,有更好的办法将方法附加给构造函数(是不会污染全局环境的哦)。

function fn1(){ var a = 1; function fn(){ a++; console.log(a); } return fn;}// 调用函数var x = fn1();x(); // 2x();//3

方法和原型

前面介绍中提到 JavaScript 是一种基于原型的编程语言。在 JavaScript
中,可以把原型当作对象模板一样来使用。原型能避免在实例化对象时创建多余的匿名函数和变量。

在 JavaScript
中,prototype 是一个非常特别的属性,可以让我们为对象添加新的属性和方法。现在用原型重写上面的示例看看:

// creating a function

function Person( name, email ) {

    // declaring properties and methods using the (this) keyword

    this.name   = name;
    this.email  = email;

}

// assign a new method to the object’s prototype

Person.prototype.sayHey = function() {

    console.log( "Hey, I’m " + this.name );

}

// instantiating a new object using the constructor function

var steve = new Person( "Steve", "steve@hotmail.com" );

// accessing methods and properties

steve.sayHey();

这个示例中,不再为每个 Person 实例定义 sayHey 方法,而是通过原型模板在各实例中共享这个方法。

闭包函数在js的开发当中是非常常见的写法,例如下面这种写法,功能是实现了对数组的一些常规操作的封装,也是属于对闭包函数的一种应用。

继承性

通过原型链,原型可以用来实例继承。JavaScript
的每一个对象都有原型,而原型是另外一个对象,也有它自己的原型,周而复始…直到某个原型对象的原型是 null——原型链到此为止。

在访问一个方法或属性的时候,JavaScript
首先检查它们是否在对象中定义,如果不,则检查是否定义在原型中。如果在原型中也没找到,则会延着原型链一直找下去,直到找到,或者到达原型链的终端。

现在来看看代码是怎么实现的。可以从上一个示例中的 Person 对象开始,另外再创建一个叫 Employee 的对象。

// Our person object

function Person( name, email ) {

    this.name   = name;
    this.email  = email;

}

Person.prototype.sayHey = function() {

    console.log( "Hey, I’m " + this.name );

}

// A new employee object

function Employee( jobTitle ) {

    this.jobTitle = jobTitle;

}

现在 Employee 只有一个属性。不过既然员工也属于人,我们希望它能从 Person 继承其它属性。要达到这个目的,我们可以在 Employee 对象中调用 Person 的构造函数,并配置原型链。

// Our person object

function Person( name, email ) {

    this.name   = name;
    this.email  = email;

}

Person.prototype.sayHey = function() {

    console.log( "Hey, I’m " + this.name );

}

// A new employee object

function Employee( name, email, jobTitle ) {

    // The call function is calling the Constructor of Person
    // and decorates Employee with the same properties

    Person.call( this, name, email );

    this.jobTitle = jobTitle;

}

// To set up the prototype chain, we create a new object using 
// the Person prototype and assign it to the Employee prototype

Employee.prototype = Object.create( Person.prototype );

// Now we can access Person properties and methods through the
// Employee object

var matthew = new Employee( "Matthew", "matthew@hotmail.com", "Developer" );

matthew.sayHey();

要适应原型继承还需要一些时间,但是这一个必须熟悉的重要概念。虽然原型继承模型常常被认为是
JavaScript
的弱点,但实际上它比传统模型更强大。比如说,在掌握了原型模型之后创建传统模型简直就太容易了。

ECMAScript 6
引入了一组新的关键字用于实现 类。虽然新的设计看起来与传统基于类的开发语言非常接近,但它们并不相同。JavaScript
仍然基于原型。

let Utils = (function(){ var list = []; return { add:function(item){ if(list.indexOf(item)-1) return; // 如果数组内元素存在,那么不在重复添加 list.push(item); }, remove:function(item){ if(list.indexOf(item)  0) return; // 如果要删除的数组数组之内不存在,那么就返回 list.splice(list.indexOf(item),1); }, get_length:function(){ return list.length; }, get_showData:function() { return list; } }})();Utils.add("hello,world");Utils.add("this is test");console.log(Utils.get_showData());// ["hello,world","this is test"]

在上面的代码中,函数嵌套函数形成了闭包函数的结构,在开发中是比较常见的写法。

闭包的概念:

闭包是指有权限访问上一级父作用域的变量的函数.

立即执行函数(IIFE)

在js开发中,经常碰到立即执行函数的写法。大体如下:

// 下面的这种写法就是立即执行函数// 在函数内部的内容会自动执行(function(){ var a = 10; var b = 20; console.log(a+b); // 30})();

我们也可以通过第二个括号内传入参数:

(function(i){ console.log(i);})(i);

这种自调用的写法本质上来讲也是一个闭包函数。

通过这种闭包函数,我们可以有效的避免变量污染等问题,从而创建一个独立的作用域。

但是问题相对来说也很明显,就是在这个独立的作用域当中,我们没有办法将其中的函数或者变量让外部访问的到。所以如果我们在外部需要访问这个立即执行函数中的变量或者方法,我们就需要通过第二个括号将window这个全局的变量对象传入,并且将需要外部访问的变量或者函数赋值给window,这样做相当于将其暴露在了全局的作用域范围之内。

需要注意的是,通常情况下我们只需要将必要的方法暴露,这样才能保证代码并不会相互产生过多的影响,从而降低耦合度。

例如:

(function (window){ var a = 10; // 私有属性 function show(){ return a++; } function sayHello(){ // 私有方法 alert("hello,world"); } window.show = show;// 将show方法暴露在外部})(window);

需要理解的是,在很多的代码中,总是在(function(){})()的最前面加上一个;,目的是为了防止合并代码的时候js将代码解析成(function(){})()(function(){})()这种情况。

闭包函数的变异

因为js的特殊性,所以很多时候我们在学习js的时候,除了js代码的语法以外,还要学习很多为了解决实际问题的方案,例如下面的这种写法就是为了实现module的写法。

例如:

var testModule = function(){ var name = "张三"; // 私有属性,外部无法访问 return { get_name:function(){ // 暴露在外部的方法 alert(name); }, set_name:function(new_name){ // 暴露在外部的方法 name = new_name; } }}

我们也可以将这种写法进行升级,和立即执行函数进行适度的结合也是常见的写法:

例如:

var blogModule = (function (my) { my.name = "zhangsan"; // 添加一些功能 my.sayHello = function(){ console.log(this.name) } return my;} (blogModule || {})); console.log(blogModule.sayHello())

自调用函数(自执行匿名函数(Self-executing anonymous
function))和立即执行函数的区别自调用函数其实也就是递归函数

自调用函数顾名思义,就是调用自身的函数,而立即执行函数则是立即会及执行的函数。

下面是二者的一些比较:

// 这是一个自执行的函数,函数内部执行自身,递归function foo() { foo(); }// 这是一个自执行的匿名函数,因为没有标示名称// 必须使用arguments.callee属性来执行自己var foo = function () { arguments.callee(); };// 这可能也是一个自执行的匿名函数,仅仅是foo标示名称引用它自身// 如果你将foo改变成其它的,你将得到一个used-to-self-execute匿名函数var foo = function () { foo(); };// 有些人叫这个是自执行的匿名函数(即便它不是),因为它没有调用自身,它只是立即执行而已。(function () { /* code */ } ());// 为函数表达式添加一个标示名称,可以方便Debug// 但一定命名了,这个函数就不再是匿名的了(function foo() { /* code */ } ());// 立即调用的函数表达式(IIFE)也可以自执行,不过可能不常用罢了(function () { arguments.callee(); } ());(function foo() { foo(); } ());

作用域与作用域链

作用域

所谓的作用域,指的就是变量和函数的可访问范围,控制着变量和函数的可见性与生命周期,在JavaScript中变量的作用域有全局作用域和函数作用域以及ES6新增加的块级作用域。

例如,在函数外部通过var关键字声明的变量就是全局变量,作用域的范围也就是全局作用域,而在函数内部通过var声明或者let声明的就是局部变量,作用域仅限于函数内部,在{}内部或者流程控制语句或者循环语句内部通过let声明的变量作用域范围则仅限于当前作用域。函数的参数在()内部,只能在函数内部使用,作用域范围也仅限于函数。同时window对象的所有属性也拥有全局作用域。

例如:

// 作用域范围var a = 10; // 全局function fn1(a,b){ // 函数fn1内 c = 30; // 全局 var x = 30; // 函数fn1内 function fn2(){ var s = "hello"; // 函数fn2内 console.log(x); // 30 函数内部可以访问外层的变量 }}for(var i =0;i10;i++){} // 循环体内声明的计数变量i也是一个全局console.log(i); // 10for(let j = 0;i10;j++){} // let 声明的计数变量j 是一个局部 console.log(j);// 出错,访问不到

执行环境上下文

上面我们说到了作用域,下面再来说下执行环境(execution context)。

什么是执行环境呢?

简单点说,执行环境定义了变量或者函数有权访问的其他的数据,并且也决定了他们各自的行为。需要知道的是,每一个执行环境当中,都有着一个与之关联的变量对象(variable
object),执行环境中定义的所有变量和函数都会保存在这个对象中,解析器在处理数据的时候就会访问这个内部对象。

而全局执行环境是最外层的一个执行环境,在web浏览器中最外层的执行环境关联的对象是window,所以我们可以这样说,所有全局变量和函数都是作为window对象的属性和方法创建的。

我们创建的每一个函数都有自己的执行环境,当执行流进行到函数的时候,函数的环境会被推入到一个函数执行栈当中,而在函数执行完毕后执行环境出栈并被销毁,保存在其中的所有变量和函数定义随之销毁,控制权返回到之前的执行环境中,全局的执行环境在应用程序退出(浏览器关闭)才会被销毁。

作用域链

当代码在一个执行环境执行之时,会创建一个变量对象的一个作用域链(scope
chain),来保证在执行环境中,对执行环境有权访问的变量和函数的有序访问。

作用域第一个也d就是顶层对象始终是当前执行代码所在环境的变量对象(VO)。

例如:

function fn1(){}

fn1在创建的时候作用域链被添加进全局对象,全局对象中拥有所有的全局变量。

例如上面的fn1在创建的时候,所处的环境是全局环境,所以此时的this就指向window。

在函数运行过程中标识符的解析是沿着作用域链一级一级搜索的过程,从第一个对象开始,逐级向后回溯,直到找到同名标识符为止,找到后不再继续遍历,找不到就报错。

如果执行环境是函数,那么将其活动对象(activation object,
AO)作为作用域链第一个对象,第二个对象是包含环境,下一个是包含环境上一层的包含环境…

也就是说所谓的作用域链,就是指具体的某个变量或者函数从其第一个对象(活动对象)一直到顶层执行环境。这中间的联系就是作用域链。

被人误解的闭包函数

谈及闭包函数的概念,经常会有人错误的将其理解为从父上下文中返回内部函数,甚至理解成只有匿名函数才能是闭包。

而实际来说,因为作用域链,使得所有的函数都是闭包(与函数类型无关:
匿名函数,FE,NFE,FD都是闭包)。

注意:这里只有一类函数除外,那就是通过Function构造器创建的函数,因为其[[Scope]]只包含全局对象。闭包函数的应用

闭包函数是js当中非常重要的概念,在诸多的地方可以应用到闭包,通过闭包,我们可以写出很多优秀的代码,下面是一些常见的内容:

例如:

// 数组排序[1, 2, 3].sort(function (a, b) { ... // 排序条件});// map方法的应用,根据函数中定义的条件将原数组映射到一个新的数组中[1, 2, 3].map(function (element) { return element * 2;}); // [2, 4, 6]// 常用的 forEach[1, 2, 3].forEach(function (element) { if (element % 2 != 0) { alert(element); }}); // 1, 3

例如我们常用的call和apply方法,它们是两个应用函数,也就是应用到参数中的函数(在apply中是参数列表,在call中是独立的参数):

例如:

(function () { alert([].join.call(arguments, ';')); // 1;2;3}).apply(this, [1, 2, 3]);

还有最常使用的写法:

var a = 10;setTimeout(function () { alert(a); // 10, after one second}, 1000);

当然,ajax的写法也就是回调函数其实本质也是闭包:

//...var x = 10;// only for examplexmlHttpRequestObject.onreadystatechange = function () { // 当数据就绪的时候,才会调用; // 这里,不论是在哪个上下文中创建 // 此时变量“x”的值已经存在了 alert(x); // 10};//...

当然也包括我们上边说的封装独立作用域的写法:

例如:

var foo = {};// 初始化(function (object) { var x = 10; object.getX = function _getX() { return x; };})(foo);alert(foo.getX()); // 获得闭包 "x" – 10

JSON对象和JS对象直接量

在工作当中,我们总是可以听到人说将数据转换为JSON对象,或者说把JSON对象转换为字符串之类的话,下面是关于JSON的具体说明。

JSON对象并不是JavaScript对象字面量(Object Literals)

很多人错误的将JSON认为是JavaScript当中的对象字面量(object
Literals),原因非常简单,就是因为它们的语法是非常相似的,但是在ECMA中明确的说明了。JSON只是一种数据交互语言,只有我们将之用在string上下文的时候它才叫JSON。

序列化与反序列化

2个程序(或服务器、语言等)需要交互通信的时候,他们倾向于使用string字符串因为string在很多语言里解析的方式都差不多。复杂的数据结构经常需要用到,并且通过各种各样的中括号{},小括号(),叫括号和空格来组成,这个字符串仅仅是按照要求规范好的字符。

为此,我们为了描述这些复杂的数据结构作为一个string字符串,制定了标准的规则和语法。JSON只是其中一种语法,它可以在string上下文里描述对象,数组,字符串,数字,布尔型和null,然后通过程序间传输,并且反序列化成所需要的格式。

常见的数据流行交互格式有YAML、XML、和JSON都是常用的数据交互格式。

字面量澳门新葡亰网站注册,引用Mozilla Developer Center里的几句话,供大家参考:

他们是固定的值,不是变量,让你从“字面上”理解脚本。
(Literals)字符串字面量是由双引号(”)或单引号(’)包围起来的零个或多个字符组成的。(Strings
Literals)对象字面量是由大括号({})括起来的零个或多个对象的属性名-值对。(Object
Literals)

什么时候会成为JSON

JSON是设计成描述数据交换格式的,他也有自己的语法,这个语法是JavaScript的一个子集。{
“prop”: “val” }
这样的声明有可能是JavaScript对象字面量也有可能是JSON字符串,取决于什么上下文使用它,如果是用在string上下文(用单引号或双引号引住,或者从text文件读取)的话,那它就是JSON字符串,如果是用在对象字面量上下文中,那它就是对象字面量。

例如:

// 这是JSON字符串var foo = '{ "prop": "val" }';// 这是对象字面量var bar = { "prop": "val" };

而且要注意,JSON有非常严格的语法,在string上下文里{ “prop”: “val” }
是个合法的JSON,但{ prop: “val” }和{ ‘prop’: ‘val’
}确实不合法的。所有属性名称和它的值都必须用双引号引住,不能使用单引号。

JS当中的JSON对象

目前,JSON对象已经成为了JS当中的一个内置对象,有两个静态的方法:JSON.parse和JSON.stringify。

JSON.parse主要要来将JSON字符串反序列化成对象,JSON.stringify用来将对象序列化成JSON字符串。老版本的浏览器不支持这个对象,但你可以通过json2.js来实现同样的功能。