JavaScript面向对象编程
面向对象编程
一、实例对象与new命令
1、对象是什么
面向对象编程(Object Oriented Programming,缩写为 OOP)是目前主流的编程范式。它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。对象可以复用,通过继承机制还可以定制。因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。
那么,“对象”(object)到底是什么?我们从两个层次来理解。
(1)对象是单个实物的抽象。
一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个与远程服务器的连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。
(2)对象是一个容器,封装了属性(property)和方法(method)。
属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal
对象,使用“属性”记录具体是那一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。
1 | var person = { // 对象 |
2、构造函数
面向对象编程的第一步,就是要生成对象。前面说过,对象是单个实物的抽象。通常需要一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成。
典型的面向对象编程语言(比如 C++ 和 Java),都有“类”(class)这个概念。所谓“类”就是对象的模板,对象就是“类”的实例。但是,JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)。
JavaScript 语言使用构造函数(constructor)作为对象的模板。
所谓”构造函数”,就是专门用来生成实例对象的函数。
它就是对象的模板,描述实例对象的基本结构。
一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。
构造函数就是一个普通的函数,但是有自己的特征和用法。
1 | var Vehicle = function () { |
上面代码中,Vehicle
就是构造函数。为了与普通函数区别,构造函数名字的第一个字母通常大写。
构造函数的特点有两个。
- 函数体内部使用了
this
关键字,代表了所要生成的对象实例。 - 生成对象的时候,必须使用
new
命令。
下面先介绍new
命令。
3、new命令
基本用法
new
命令的作用:执行构造函数,返回一个实例对象。
1 | var Vehicle = function () { |
上面代码通过new
命令,让构造函数Vehicle
生成一个实例对象,保存在变量v
中。这个新生成的实例对象,从构造函数Vehicle
得到了price
属性。new
命令执行时,构造函数内部的this
,就代表了新生成的实例对象,this.price
表示实例对象有一个price
属性,值是1000。
使用new
命令时,根据需要,构造函数也可以接受参数。
1 | var Vehicle = function (p) { |
new
命令本身就可以执行构造函数,所以后面的构造函数可以带括号,也可以不带括号。下面两行代码是等价的,但是为了表示这里是函数调用,推荐使用括号。
1 | // 推荐的写法 |
一个很自然的问题是,如果忘了使用new
命令,直接调用构造函数会发生什么事?
这种情况下,构造函数就变成了普通函数,并不会生成实例对象。而且由于后面会说到的原因,this
这时代表全局对象,将造成一些意想不到的结果。
1 | var Vehicle = function (){ |
上面代码中,调用Vehicle
构造函数时,忘了加上new
命令。结果,变量v
变成了undefined
,而price
属性变成了全局变量。因此,应该非常小心,避免不使用new
命令、直接调用构造函数。
为了保证构造函数必须与new
命令一起使用,一个解决办法是,构造函数内部使用严格模式,即第一行加上use strict
。这样的话,一旦忘了使用new
命令,直接调用构造函数就会报错。
1 | function Fubar(foo, bar){ |
上面代码的Fubar
为构造函数,use strict
命令保证了该函数在严格模式下运行。由于严格模式中,函数内部的this
不能指向全局对象,默认等于undefined
,导致不加new
调用会报错(JavaScript 不允许对undefined
添加属性)。
另一个解决办法,构造函数内部判断是否使用new
命令,如果发现没有使用,则直接返回一个实例对象。
1 | function Fubar(foo, bar) { |
上面代码中的构造函数,不管加不加new
命令,都会得到同样的结果。
new 命令的原理
使用new
命令时,它后面的函数依次执行下面的步骤。
创建一个空对象,作为将要返回的实例对象。
将这个空对象的原型,指向构造函数的
prototype
属性。将这个空对象赋值给函数内部的
this
关键字。开始执行构造函数内部的代码。(代码中this指向空对象(实例对象))
返回实例对象(或自定义对象)
也就是说,构造函数内部,this
指的是一个新生成的空对象,所有针对this
的操作,都会发生在这个空对象上。构造函数之所以叫“构造函数”,就是说这个函数的目的,就是操作一个空对象(即this
对象),将其“构造”为需要的样子。
如果构造函数内部有return
语句,而且return
后面跟着一个对象,new
命令会返回return
语句指定的对象;否则,就会不管return
语句,返回this
对象。
1 | var Vehicle = function () { |
上面代码中,构造函数Vehicle
的return
语句返回一个数值。这时,new
命令就会忽略这个return
语句,返回“构造”后的this
对象。
但是,如果return
语句返回的是一个跟this
无关的新对象,new
命令会返回这个新对象,而不是this
对象。这一点需要特别引起注意。
1 | var Vehicle = function (){ |
上面代码中,构造函数Vehicle
的return
语句,返回的是一个新对象。new
命令会返回这个对象,而不是this
对象。
另一方面,如果对普通函数(内部没有this
关键字的函数)使用new
命令,则会返回一个空对象。
1 | function getMessage() { // 内部沒有this关键字,会返回一个空对象 |
上面代码中,getMessage
是一个普通函数,返回一个字符串。对它使用new
命令,会得到一个空对象。这是因为**new
命令总是返回一个对象,要么是实例对象,要么是return
语句指定的对象**。本例中,return
语句返回的是字符串,所以new
命令就忽略了该语句。
new
命令简化的内部流程,可以用下面的代码表示。
1 | // 构造函数 |
new.target 属性
构造函数内部可以使用new.target
属性。如果当前函数是new
命令调用,new.target
指向当前函数,否则为undefined
。
1 | function f() { |
使用这个属性,可以判断函数调用的时候,是否使用new
命令。
1 | function f() { |
上面代码中,构造函数f
调用时,没有使用new
命令,就抛出一个错误。
4、Object.create()创建实例对象
构造函数作为模板,可以生成实例对象。但是,有时拿不到构造函数,只能拿到一个现有的对象。我们希望以这个现有的对象作为模板,生成新的实例对象,这时就可以使用Object.create()
方法。
1 | // 这个例子没有构造函数,只有一个对象 |
上面代码中,对象person1
是person2
的模板,后者继承了前者的属性和方法。
Object.create()
的详细介绍,请看后面的相关章节。
二、this关键字
1、涵义
this
关键字是一个非常重要的语法点。毫不夸张地说,不理解它的含义,大部分开发任务都无法完成。
前一章已经提到,this
可以用在构造函数之中,表示实例对象。除此之外,this
还可以用在别的场合。但不管是什么场合,this
都有一个共同点:它总是返回一个对象。
简单说,**this
就是属性或方法“当前”所在的对象**。
1 | this.property // this代表property属性当前所在的对象 |
上面代码中,this
就代表property
属性当前所在的对象。
下面是一个实际的例子。
1 | var person = { |
上面代码中,this.name
表示name
属性所在的那个对象。由于this.name
是在describe
方法中调用,而describe
方法所在的当前对象是person
,因此this
指向person
,this.name
就是person.name
。
由于对象的属性可以赋给另一个对象,所以属性所在的当前对象是可变的,即this
的指向是可变的。
1 | var A = { |
上面代码中,A.describe
属性被赋给B
,于是B.describe
就表示describe
方法所在的当前对象是B
,所以this.name
就指向B.name
。
稍稍重构这个例子,this
的动态指向就能看得更清楚。
1 | function f() { |
上面代码中,函数f
内部使用了this
关键字,随着f
所在的对象不同,this
的指向也不同。
只要函数被赋给另一个变量,this
的指向就会变。
1 | var A = { |
上面代码中,A.describe
被赋值给变量f
,内部的this
就会指向f
运行时所在的对象(本例是顶层对象)。
再看一个网页编程的例子。
1 | <input type="text" name="age" size=3 onChange="validate(this, 18, 99);"> |
上面代码是一个文本输入框,每当用户输入一个值,就会调用onChange
回调函数,验证这个值是否在指定范围。浏览器会向回调函数传入当前对象,因此this
就代表传入当前对象(即文本框),然后就可以从this.value
上面读到用户的输入值。
总结一下,JavaScript 语言之中,一切皆对象,运行环境也是对象,所以函数都是在某个对象之中运行,this
就是函数运行时所在的对象(环境)。这本来并不会让用户糊涂,但是 JavaScript 支持运行环境动态切换,也就是说,this
的指向是动态的,没有办法事先确定到底指向哪个对象,这才是最让初学者感到困惑的地方。
2、实质
JavaScript 语言之所以有 this 的设计,跟内存里面的数据结构有关系。
1 | var obj = { foo: 5 }; |
上面的代码将一个对象赋值给变量obj
。JavaScript 引擎会先在内存里面,生成一个对象{ foo: 5 }
,然后把这个对象的内存地址赋值给变量obj
。也就是说,变量obj
是一个地址(reference)。后面如果要读取obj.foo
,引擎先从obj
拿到内存地址,然后再从该地址读出原始的对象,返回它的foo
属性。
原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来说,上面例子的foo
属性,实际上是以下面的形式保存的。
1 | { |
注意,foo
属性的值保存在属性描述对象的value
属性里面。
这样的结构是很清晰的,问题在于属性的值可能是一个函数。
1 | var obj = { foo: function () {} }; |
这时,引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo
属性的value
属性。
1 | { |
由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。
1 | var f = function () {}; |
JavaScript 允许在函数体内部,引用当前环境的其他变量。
1 | var f = function () { |
上面代码中,函数体里面使用了变量x
。该变量由运行环境提供。
现在问题就来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this
就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。
1 | var f = function () { |
上面代码中,函数体里面的this.x
就是指当前运行环境的x
。
1 | var f = function () { |
上面代码中,函数f
在全局环境执行,this.x
指向全局环境的x
;在obj
环境执行,this.x
指向obj.x
。
3、使用场合
this
主要有以下几个使用场合。
(1)全局环境
全局环境使用this
,它指的就是顶层对象window
。
1 | this === window // true |
上面代码说明,不管是不是在函数内部,只要是在全局环境下运行,this
就是指顶层对象window
。
(2)构造函数
构造函数中的this
,指的是实例对象。
1 | var Obj = function (p) { |
上面代码定义了一个构造函数Obj
。由于this
指向实例对象,所以在构造函数内部定义this.p
,就相当于定义实例对象有一个p
属性。
(3)对象的方法
如果对象的方法里面包含this
,this
的指向就是方法运行时所在的对象。该方法赋值给另一个对象,就会改变this
的指向。
但是,这条规则很不容易把握。请看下面的代码。
1 | var obj ={ |
上面代码中,obj.foo
方法执行时,它内部的this
指向obj
。
但是,下面这几种用法,都会改变this
的指向。
1 | // 情况一 |
上面代码中,obj.foo
就是一个值。这个值真正调用的时候,运行环境已经不是obj
了,而是全局环境,所以this
不再指向obj
。
可以这样理解,JavaScript 引擎内部,**obj
和obj.foo
储存在两个内存地址,称为地址一和地址二。obj.foo()
这样调用时,是从地址一调用地址二,因此地址二的运行环境是地址一,this
指向obj
。但是,上面三种情况,都是直接取出地址二进行调用,这样的话,运行环境就是全局环境,因此this
指向全局环境**。上面三种情况等同于下面的代码。
1 | //obj.foo 是一个内存地址,它直接取出了 function () {console.log(this);} ,可以把obj.foo看做function () {console.log(this);} |
如果this
所在的方法不在对象的第一层,这时this
只是指向当前一层的对象,而不会继承更上面的层。
1 | var a = { |
上面代码中,a.b.m
方法在a
对象的第二层,该方法内部的this
不是指向a
,而是指向a.b
,因为实际执行的是下面的代码。
1 | var b = { |
如果要达到预期效果,只有写成下面这样。
1 | var a = { |
如果这时将嵌套对象内部的方法赋值给一个变量,this
依然会指向全局对象。
1 | var a = { |
上面代码中,m
是多层对象内部的一个方法。为求简便,将其赋值给hello
变量,结果调用时,this
指向了顶层对象。为了避免这个问题,可以只将m
所在的对象赋值给hello
,这样调用时,this
的指向就不会变。
1 | var hello = a.b; |
4、使用注意点
(1)避免多层 this
由于this
的指向是不确定的,所以切勿在函数中包含多层的this
。
1 | var o = { |
上面代码包含两层this
,结果运行后,第一层指向对象o
,第二层指向全局对象,因为实际执行的是下面的代码。
1 | var temp = function () { |
一个解决方法是在第二层改用一个指向外层this
的变量。
1 | var o = { |
上面代码定义了变量that
,固定指向外层的this
,然后在内层使用that
,就不会发生this
指向的改变。
事实上,使用一个变量固定this
的值,然后内层函数调用这个变量,是非常常见的做法,请务必掌握。
JavaScript 提供了严格模式,也可以硬性避免这种问题。严格模式下,如果函数内部的this
指向顶层对象,就会报错。
1 | var counter = { |
上面代码中,inc
方法通过'use strict'
声明采用严格模式,这时内部的this
一旦指向顶层对象,就会报错。
(2)避免数组处理方法中的 this
数组的map
和foreach
方法,允许提供一个函数作为参数。这个函数内部不应该使用this
。
1 | var o = { |
上面代码中,foreach
方法的回调函数中的this
,其实是指向window
对象,因此取不到o.v
的值。原因跟上一段的多层this
是一样的,就是内层的this
不指向外部,而指向顶层对象。
解决这个问题的一种方法,就是前面提到的,使用中间变量固定this
。
1 | var o = { |
另一种方法是将this
当作foreach
方法的第二个参数,固定它的运行环境。
1 | var o = { |
ES6箭头函数
1 | var o = { |
(3)避免回调函数中的 this
回调函数中的this
往往会改变指向,最好避免使用。
1 | var o = new Object(); |
上面代码中,点击按钮以后,控制台会显示false
。原因是此时this
不再指向o
对象,而是指向按钮的 DOM 对象,因为f
方法是在按钮对象的环境中被调用的。这种细微的差别,很容易在编程中忽视,导致难以察觉的错误。
为了解决这个问题,可以采用下面的一些方法对this
进行绑定,也就是使得this
固定指向某个对象,减少不确定性。
5、绑定 this 的方法
this
的动态切换,固然为 JavaScript 创造了巨大的灵活性,但也使得编程变得困难和模糊。有时,需要把this
固定下来,避免出现意想不到的情况。JavaScript 提供了call
、apply
、bind
这三个方法,来切换/固定this
的指向。
Function.prototype.call()
函数实例的call
方法,可以指定函数内部this
的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。
1 | var obj = {}; |
上面代码中,全局环境运行函数f
时,this
指向全局环境(浏览器为window
对象);call
方法可以改变this
的指向,指定this
指向对象obj
,然后在对象obj
的作用域中运行函数f
。
call
方法的参数,应该是一个对象。如果参数为空、null
和undefined
,则默认传入全局对象。
1 | var n = 123; |
上面代码中,a
函数中的this
关键字,如果指向全局对象,返回结果为123
。如果使用call
方法将this
关键字指向obj
对象,返回结果为456
。可以看到,如果call
方法没有参数,或者参数为null
或undefined
,则等同于指向全局对象。
如果call
方法的参数是一个原始值,那么这个原始值会自动转成对应的包装对象,然后传入call
方法。
1 | var f = function () { |
上面代码中,call
的参数为5
,不是对象,会被自动转成包装对象(Number
的实例),绑定f
内部的this
。
call
方法还可以接受多个参数。
语法
1 | func.call(thisValue, arg1, arg2, ...) |
call
的第一个参数就是this
所要指向的那个对象,后面的参数则是函数调用时所需的参数。
1 | function add(a, b) { |
上面代码中,call
方法指定函数add
内部的this
绑定当前环境(对象),并且参数为1
和2
,因此函数add
运行后得到3
。
应用:调用对象的原生方法
call
方法的一个应用是调用对象的原生方法。
1 | var obj = {}; |
上面代码中,hasOwnProperty
是obj
对象继承的方法,如果这个方法一旦被覆盖,就不会得到正确结果。call
方法可以解决这个问题,它将hasOwnProperty
方法的原始定义放到obj
对象上执行,这样无论obj
上有没有同名方法,都不会影响结果。
1 | var objArr = { // 类数组对象 |
上面代码中,objArr
是一个类似数组的对象,使用call
调用数组的slice
方法,指定objArr
为slice
方法内部的this
,slice
方法返回值赋给arr
。
另外来看看数组slice
方法的内部实现原理
1 | Array.prototype.slice=function(start,end){ //数组方法slice的底层内部实现 |
Function.prototype.apply()
apply
方法的作用与call
方法类似,也是改变this
指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数,使用格式如下。
语法
1 | func.apply(thisValue, [arg1, arg2, ...]) |
apply
方法的第一个参数也是this
所要指向的那个对象,如果设为null
或undefined
,则等同于指定全局对象。第二个参数则是一个数组,该数组的所有成员依次作为参数,传入原函数。原函数的参数,在call
方法中必须一个个添加,但是在apply
方法中,必须以数组形式添加。
1 | function f(x, y){ |
上面代码中,f
函数本来接受两个参数,使用apply
方法以后,就变成可以接受一个数组作为参数。
利用这一点,可以做一些有趣的应用。
应用:
(1)获取数组最大/最小元素
JavaScript 不提供找出数组最大元素的函数。结合使用apply
方法和Math.max/Math.min
方法,就可以返回数组的最大/最小元素。
利用第二个参数为数组的特点
1 | // 获取最大值 |
(2)将数组的空元素变为undefined
通过apply
方法,利用Array
构造函数将数组的空元素变成undefined
。
1 | Array.apply(null, ['a', ,'b']) |
空元素与undefined
的差别在于,数组的forEach
方法会跳过空元素,但是不会跳过undefined
。因此,遍历内部元素的时候,会得到不同的结果。
1 | var a = ['a', , 'b']; |
(3)转换类似数组的对象
另外,利用数组对象的slice
方法,可以将一个类似数组的对象(比如arguments
对象)转为真正的数组。
和使用call
方法的效果一样。
1 | Array.prototype.slice.apply({0: 1, length: 1}) // [1] |
上面代码的apply
方法的参数都是对象,但是返回结果都是数组,这就起到了将对象转成数组的目的。从上面代码可以看到,这个方法起作用的前提是,被处理的对象必须有length
属性,以及相对应的数字键。
(4)绑定回调函数的对象
前面的按钮点击事件的例子,可以改写如下。
1 | var o = new Object(); |
上面代码中,点击按钮以后,控制台将会显示true
。由于apply
方法(或者call
方法)不仅绑定函数执行时所在的对象,还会立即执行函数,因此不得不把绑定语句写在一个函数体内。更简洁的写法是采用下面介绍的bind
方法。
Function.prototype.bind()
bind
方法用于将函数体内的this
绑定到某个对象,然后返回一个新函数,bind方法并非立即执行一个函数。
语法
1 | func.bind(thisValue, arg1, arg2, ...)() |
1 | var d = new Date(); |
上面代码中,我们将d.getTime
方法赋给变量print
,然后调用print
就报错了。这是因为getTime
方法内部的this
,绑定Date
对象的实例,赋给变量print
以后,内部的this
已经不指向Date
对象的实例了。
bind
方法可以解决这个问题。
1 | var print = d.getTime.bind(d); |
上面代码中,bind
方法将getTime
方法内部的this
绑定到d
对象,这时就可以安全地将这个方法赋值给其他变量了。
bind
方法的参数就是所要绑定this
的对象,下面是一个更清晰的例子。
1 | var counter = { |
上面代码中,counter.inc
方法被赋值给变量func
。这时必须用bind
方法将inc
内部的this
,绑定到counter
,否则就会出错。
this
绑定到其他对象也是可以的。
1 | var counter = { |
上面代码中,bind
方法将inc
方法内部的this
,绑定到obj
对象。结果调用func
函数以后,递增的就是obj
内部的count
属性。
bind
还可以接受更多的参数,将这些参数绑定原函数的参数。
1 | var add = function (x, y) { |
上面代码中,bind
方法除了绑定this
对象,还将add
函数的第一个参数x
绑定成5
,然后返回一个新函数newAdd
,这个函数只要再接受一个参数y
就能运行了。
如果bind
方法的第一个参数是null
或undefined
,等于将this
绑定到全局对象,函数运行时this
指向顶层对象(浏览器为window
)。
1 | function add(x, y) { |
上面代码中,函数add
内部并没有this
,使用bind
方法的主要目的是绑定参数x
,以后每次运行新函数plus5
,就只需要提供另一个参数y
就够了。而且因为add
内部没有this
,所以bind
的第一个参数是null
,不过这里如果是其他对象,也没有影响。
bind要注意的点:
(1)每一次返回一个新函数
bind
方法每运行一次,就返回一个新函数,这会产生一些问题。比如,监听事件的时候,不能写成下面这样。
1 | element.addEventListener('click', o.m.bind(o)); |
上面代码中,click
事件绑定bind
方法生成的一个匿名函数。这样会导致无法取消绑定,所以,下面的代码是无效的。
1 | element.removeEventListener('click', o.m.bind(o)); |
正确的方法是写成下面这样:
1 | var listener = o.m.bind(o); |
(2)结合回调函数使用
回调函数是 JavaScript 最常用的模式之一,但是一个常见的错误是,将包含this
的方法直接当作回调函数。解决方法就是使用bind
方法,将counter.inc
绑定counter
。
1 | var counter = { |
上面代码中,callIt
方法会调用回调函数。这时如果直接把counter.inc
传入,调用时counter.inc
内部的this
就会指向全局对象。使用bind
方法将counter.inc
绑定counter
以后,就不会有这个问题,this
总是指向counter
。
还有一种情况比较隐蔽,就是某些数组方法可以接受一个函数当作参数。这些函数内部的this
指向,很可能也会出错。
1 | var obj = { |
上面代码中,obj.print
内部this.times
的this
是指向obj
的,这个没有问题。但是,forEach
方法的回调函数内部的this.name
却是指向全局对象,导致没有办法取到值。稍微改动一下,就可以看得更清楚。
1 | obj.print = function () { |
解决这个问题,也是通过bind
方法绑定this
。
1 | obj.print = function () { |
(3)结合call
方法使用(改写数组方法调用形式)
利用bind
方法,可以改写一些 JavaScript 原生方法的使用形式,以数组的slice
方法为例。
1 | [1, 2, 3].slice(0, 1) // [1] |
上面的代码中,数组的slice
方法从[1, 2, 3]
里面,按照指定位置和长度切分出另一个数组。这样做的本质是在[1, 2, 3]
上面调用Array.prototype.slice
方法,因此可以用call
方法表达这个过程,得到同样的结果。
call
方法实质上是调用Function.prototype.call
方法,因此上面的表达式可以用bind
方法改写。
1 | var slice = Function.prototype.call.bind(Array.prototype.slice); |
上面代码的含义就是,将Array.prototype.slice
变成Function.prototype.call
方法所在的对象,调用时就变成了Array.prototype.slice.call
。类似的写法还可以用于其他数组方法。
1 | var push = Function.prototype.call.bind(Array.prototype.push); |
如果再进一步,将Function.prototype.call
方法绑定到Function.prototype.bind
对象,就意味着bind
的调用形式也可以被改写。
1 | function f() { |
上面代码的含义就是,将Function.prototype.bind
方法绑定在Function.prototype.call
上面,所以bind
方法就可以直接使用,不需要在函数实例上使用。
总结,call()、apply()、bind()的区别
这三个方法都是改变函数内部this指向的。
它们的区别是:
call()第一个参数为函数内this将要指向的对象,其余参数为传入函数的参数。
apply()第一个参数为函数内this将要指向的对象,第二个参数为数组,数组每一项为传入函数的参数。
bind()传入参数和call()一样,区别是bind()返回一个新的函数,并非立即执行。
三、对象的继承
面向对象编程很重要的一个方面,就是对象的继承。A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。这对于代码的复用是非常有用的。
大部分面向对象的编程语言,都是通过“类”(class)实现对象的继承。传统上,JavaScript 语言的继承不通过 class,而是通过“原型对象”(prototype)实现,本章介绍 JavaScript 的原型链继承。
ES6 引入了 class 语法,基于 class 的继承不在这个教程介绍,请参阅《ES6 标准入门》一书的相关章节。
ES5 是通过“原型对象”(prototype)实现继承。
1、原型对象概述
(1)构造函数的缺点
JavaScript 通过构造函数生成新对象,因此构造函数可以视为对象的模板。实例对象的属性和方法,可以定义在构造函数内部。
1 | function Cat (name, color) { |
上面代码中,Cat
函数是一个构造函数,函数内部定义了name
属性和color
属性,所有实例对象(上例是cat1
)都会生成这两个属性,即这两个属性会定义在实例对象上面。
通过构造函数为实例对象定义属性,虽然很方便,但是有一个缺点。同一个构造函数的多个实例之间,无法共享属性,从而造成对系统资源的浪费。
1 | function Cat(name, color) { |
上面代码中,cat1
和cat2
是同一个构造函数的两个实例,它们都具有meow
方法。由于meow
方法是生成在每个实例对象上面,所以两个实例就生成了两次。也就是说,每新建一个实例,就会新建一个meow
方法。这既没有必要,又浪费系统资源,因为所有meow
方法都是同样的行为,完全应该共享。
这个问题的解决方法,就是 JavaScript 的原型对象(prototype)。
(2)prototype 属性的作用
JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系。
下面,先看怎么为对象指定原型。JavaScript 规定,每个函数都有一个prototype
属性,指向一个对象。
1 | function f() {} |
上面代码中,函数f
默认具有prototype
属性,指向一个对象。
对于普通函数来说,该属性基本无用。但是,对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。
1 | function Animal(name) { |
上面代码中,构造函数Animal
的prototype
属性,就是实例对象cat1
和cat2
的原型对象。原型对象上添加一个color
属性,结果,实例对象都共享了该属性。
原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在所有实例对象上。
1 | Animal.prototype.color = 'yellow'; |
上面代码中,原型对象的color
属性的值变为yellow
,两个实例对象的color
属性立刻跟着变了。这是因为实例对象其实没有color
属性,都是读取原型对象的color
属性。也就是说,当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。这就是原型对象的特殊之处。
如果实例对象自身就有某个属性或方法,它就不会再去原型对象寻找这个属性或方法。
1 | cat1.color = 'black'; |
上面代码中,实例对象cat1
的color
属性改为black
,就使得它不再去原型对象读取color
属性,后者的值依然为yellow
。
总结一下,原型对象的作用,就是定义所有实例对象共享的属性和方法。这也是它被称为原型对象的原因,而实例对象可以视作从原型对象衍生出来的子对象。
1 | Animal.prototype.walk = function () { |
上面代码中,Animal.prototype
对象上面定义了一个walk
方法,这个方法将可以在所有Animal
实例对象上面调用。
(3)原型链
JavaScript 规定,所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型……
如果一层层地上溯,所有对象的原型最终都可以上溯到Object.prototype
,即Object
构造函数的prototype
属性。也就是说,所有对象都继承了Object.prototype
的属性。这就是所有对象都有valueOf
和toString
方法的原因,因为这是从Object.prototype
继承的。
那么,Object.prototype
对象有没有它的原型呢?回答是Object.prototype
的原型是null
。null
没有任何属性和方法,也没有自己的原型。因此,**原型链的尽头就是null
**。
1 | Object.getPrototypeOf(Object.prototype) // Object.getPrototypeOf方法返回对象的原型 |
上面代码表示,Object.prototype
对象的原型是null
,由于null
没有任何属性,所以原型链到此为止。Object.getPrototypeOf
方法返回参数对象的原型,具体介绍请看后文。
**读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype
还是找不到,则返回undefined
**。如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overriding)。
注意,一级级向上,在整个原型链上寻找某个属性,对性能是有影响的。所寻找的属性在越上层的原型对象,对性能的影响越大。如果寻找某个不存在的属性,将会遍历整个原型链。
举例来说,如果让构造函数的prototype
属性指向一个数组,就意味着实例对象可以调用数组方法。
1 | var MyArray = function () {}; |
上面代码中,mine
是构造函数MyArray
的实例对象,由于MyArray.prototype
指向一个数组实例,使得mine
可以调用数组方法(这些方法定义在数组实例的prototype
对象上面)。最后那行instanceof
表达式,用来比较一个对象是否为某个构造函数的实例,结果就是证明mine
为Array
的实例,instanceof
运算符的详细解释详见后文。
上面代码还出现了原型对象的constructor
属性,这个属性的含义下一节就来解释。
(4)constructor 属性
prototype
对象有一个constructor
属性,默认指向prototype
对象所在的构造函数。
1 | function P() {} |
由于constructor
属性定义在prototype
对象上面,意味着可以被所有实例对象继承。
1 | function P() {} |
上面代码中,p
是构造函数P
的实例对象,但是p
自身没有constructor
属性,该属性其实是读取原型链上面的P.prototype.constructor
属性。
作用
constructor
属性的作用是,可以得知某个实例对象,到底是哪一个构造函数产生的。
1 | function F() {}; |
上面代码中,constructor
属性确定了实例对象f
的构造函数是F
,而不是RegExp
。
另一方面,有了constructor
属性,就可以从一个实例对象新建另一个实例。
1 | function Constr() {} |
上面代码中,x
是构造函数Constr
的实例,可以从x.constructor
间接调用构造函数。这使得在实例方法中,调用自身的构造函数成为可能。
1 | Constr.prototype.createCopy = function () { |
上面代码中,createCopy
方法调用构造函数,新建另一个实例。
constructor
属性表示原型对象与构造函数之间的关联关系,如果修改了原型对象,一般会同时修改constructor
属性,防止引用的时候出错。
1 | function Person(name) { |
上面代码中,构造函数Person
的原型对象改掉了,但是没有修改constructor
属性,导致这个属性不再指向Person
。由于Person
的新原型是一个普通对象,而普通对象的constructor
属性指向Object
构造函数,导致Person.prototype.constructor
变成了Object
。
所以,修改原型对象时,一般要同时修改constructor
属性的指向。
1 | // 坏的写法 |
上面代码中,要么将constructor
属性重新指向原来的构造函数,要么只在原型对象上添加方法,这样可以保证instanceof
运算符不会失真。
如果不能确定constructor
属性是什么函数,还有一个办法:通过name
属性,从实例得到构造函数的名称。
1 | function Foo() {} |
2、instanceof 运算符
instanceof
运算符,判断对象是否为某个构造函数的实例,返回一个布尔值。
语法
1 | <实例对象> instanceof <构造函数> |
1 | var v = new Vehicle(); |
上面代码中,对象v
是构造函数Vehicle
的实例,所以返回true
。
instanceof
运算符的左边是实例对象,右边是构造函数。它会检查右边构造函数的原型对象(prototype),是否在左边对象的原型链上。因此,下面两种写法是等价的。
1 | v instanceof Vehicle |
上面代码中,Object.prototype.isPrototypeOf
的详细解释见后文。
由于instanceof
检查整个原型链,因此同一个实例对象,可能会对多个构造函数都返回true
。
1 | var d = new Date(); |
上面代码中,d
同时是Date
和Object
的实例,因此对这两个构造函数都返回true
。
由于任意对象(除了null
)都是Object
的实例,所以instanceof
运算符可以判断一个值是否为非null
的对象。
1 | var obj = { foo: 123 }; |
上面代码中,除了null
,其他对象的instanceOf Object
的运算结果都是true
。
instanceof
的原理是检查右边构造函数的prototype
属性,是否在左边对象的原型链上。有一种特殊情况,就是左边对象的原型链上,只有null
对象。这时,instanceof
判断会失真。
1 | var obj = Object.create(null); |
上面代码中,Object.create(null)
返回一个新对象obj
,它的原型是null
(Object.create
的详细介绍见后文)。右边的构造函数Object
的prototype
属性,不在左边的原型链上,因此instanceof
就认为obj
不是Object
的实例。但是,只要一个对象的原型不是null
,instanceof
运算符的判断就不会失真。
用处
instanceof
运算符的一个用处,是判断值的类型。
1 | var x = [1, 2, 3]; |
上面代码中,instanceof
运算符判断,变量x
是数组,变量y
是对象,变量z
是函数。
注意,instanceof
运算符只能用于对象,不适用原始类型的值。
1 | var s = 'hello'; |
上面代码中,字符串不是String
对象的实例(因为字符串不是对象),所以返回false
。
此外,对于undefined
和null
,instanceof
运算符总是返回false
。
1 | undefined instanceof Object // false |
利用instanceof
运算符,还可以巧妙地解决,调用构造函数时,忘了加new
命令的问题。
1 | function Fubar (foo, bar) { |
上面代码使用instanceof
运算符,在函数体内部判断this
关键字是否为构造函数Fubar
的实例。如果不是,就表明忘了加new
命令。
3、构造函数的继承
让一个构造函数继承另一个构造函数,是非常常见的需求。
这可以分成两步实现。第一步是在子类的构造函数中,调用父类的构造函数。
1 | function Sub(value) { // Sub是子类构造函数 |
上面代码中,Sub
是子类的构造函数,this
是子类的实例。在实例上调用父类的构造函数Super
,就会让子类实例具有父类实例的属性。
第二步,是让子类的原型指向父类的原型,这样子类就可以继承父类原型。
1 | // 子类的原型指向一个新对象,新对象原型指向父类原型,等于子类原型继承了父类原型,且对子类原型操作不会影响到父类原型 |
上面代码中,Sub.prototype
是子类的原型,要将它赋值为Object.create(Super.prototype)
,而不是直接等于Super.prototype
。否则后面两行对Sub.prototype
的操作,会连父类的原型Super.prototype
一起修改掉。
另外一种写法是Sub.prototype
等于一个父类实例。
1 | Sub.prototype = new Super();// 这个写法会继承父类实例的方法,不推荐 |
上面这种写法也有继承的效果,但是子类会具有父类实例的方法。有时,这可能不是我们需要的,所以不推荐使用这种写法。
举例来说,下面是一个Shape
构造函数。
1 | function Shape() { |
我们需要让Rectangle
构造函数继承Shape
。
1 | // 第一步,子类继承父类的实例 |
采用这样的写法以后,instanceof
运算符会对子类和父类的构造函数,都返回true
。
1 | var rect = new Rectangle(); |
上面代码中,子类是整体继承父类。有时只需要单个方法的继承,这时可以采用下面的写法。
1 | ClassB.prototype.print = function() {// 单个方法的继承 |
上面代码中,子类B
的print
方法先调用父类A
的print
方法,再部署自己的代码。这就等于继承了父类A
的print
方法。
4、多重继承
JavaScript 不提供多重继承功能,即不允许一个对象同时继承多个对象。但是,可以通过变通方法,实现这个功能。
1 | function M1() { // 构造函数M1 |
上面代码中,子类S
同时继承了父类M1
和M2
。这种模式又称为 Mixin(混入)。
5、模块
随着网站逐渐变成“互联网应用程序”,嵌入网页的 JavaScript 代码越来越庞大,越来越复杂。网页越来越像桌面程序,需要一个团队分工协作、进度管理、单元测试等等……开发者必须使用软件工程的方法,管理网页的业务逻辑。
JavaScript 模块化编程,已经成为一个迫切的需求。理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。
但是,JavaScript 不是一种模块化编程语言,ES6 才开始支持“类”和“模块”。下面介绍传统的做法,如何利用对象实现模块的效果。
(1)基本的实现方法
模块是实现特定功能的一组属性和方法的封装。
简单的做法是把模块写成一个对象,所有的模块成员都放到这个对象里面。
1 | var module1 = new Object({ |
上面的函数m1
和m2
,都封装在module1
对象里。使用的时候,就是调用这个对象的属性。
1 | // 使用 |
但是,这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值。
1 | module1._count = 5; |
(2)封装私有变量:
(2-1)构造函数的写法
我们可以利用构造函数,封装私有变量。
1 | function StringBuilder() { |
上面代码中,buffer
是模块的私有变量。一旦生成实例对象,外部是无法直接访问buffer
的。但是,这种方法将私有变量封装在构造函数中,导致构造函数与实例对象是一体的,总是存在于内存之中,无法在使用完成后清除。这意味着,构造函数有双重作用,既用来塑造实例对象,又用来保存实例对象的数据,违背了构造函数与实例对象在数据上相分离的原则(即实例对象的数据,不应该保存在实例对象以外)。同时,非常耗费内存。
1 | function StringBuilder() { |
这种方法将私有变量放入实例对象中,好处是看上去更自然,但是它的私有变量可以从外部读写,不是很安全。
(2-2)立即执行函数的写法
另一种做法是使用“立即执行函数”(Immediately-Invoked Function Expression,IIFE),将相关的属性和方法封装在一个函数作用域里面,可以达到不暴露私有成员的目的。
1 | var module1 = (function () { |
使用上面的写法,外部代码无法读取内部的_count
变量。
1 | console.info(module1._count); //undefined |
上面的module1
就是 JavaScript 模块的基本写法。下面,再对这种写法进行加工。
(3)模块的放大模式(向模块添加新方法)
如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用“放大模式”(augmentation)。
1 | var module1 = (function (mod){ |
上面的代码为module1
模块添加了一个新方法m3()
,然后返回新的module1
模块。
在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道哪个部分会先加载。如果采用上面的写法,第一个执行的部分有可能加载一个不存在空对象,这时就要采用”宽放大模式”(Loose augmentation)。
1 | var module1 = (function (mod) { |
与”放大模式”相比,“宽放大模式”就是“立即执行函数”的参数可以是空对象。
(4)输入全局变量(保证独立性)
独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。
为了在模块内部调用全局变量,必须显式地将其他变量输入模块。
1 | var module1 = (function ($, YAHOO) { |
上面的module1
模块需要使用 jQuery 库和 YUI 库,就把这两个库(其实是两个模块)当作参数输入module1
。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。
立即执行函数还可以起到命名空间的作用。
1 | (function($, window, document) { |
上面代码中,finalCarousel
对象输出到全局,对外暴露init
和destroy
接口,内部方法go
、handleEvents
、initialize
、dieCarouselDie
都是外部无法调用的。
四、Object 对象的相关方法
JavaScript 在Object
对象上面,提供了很多相关方法,处理面向对象编程的相关操作。本章介绍这些方法。
1、Object.getPrototypeOf() 获取原型对象
Object.getPrototypeOf
方法返回参数对象的原型。这是获取原型对象的标准方法。
1 | var F = function () {}; |
上面代码中,实例对象f
的原型是F.prototype
。
下面是几种特殊对象的原型。
1 | // 空对象的原型是 Object.prototype |
2、Object.setPrototypeOf() 设置原型对象
Object.setPrototypeOf
方法为参数对象设置原型,返回该参数对象。它接受两个参数,第一个是现有对象,第二个是原型对象。
1 | var a = {}; |
上面代码中,Object.setPrototypeOf
方法将对象a
的原型,设置为对象b
,因此a
可以共享b
的属性。
使用Object.setPrototypeOf
方法模拟new
命令
1 | var F = function () { |
上面代码中,new
命令新建实例对象,其实可以分成两步。第一步,将一个空对象的原型设为构造函数的prototype
属性(上例是F.prototype
);第二步,将构造函数内部的this
绑定这个空对象,然后执行构造函数,使得定义在this
上面的方法和属性(上例是this.foo
),都转移到这个空对象上。
3、 Object.create() 创建实例对象,指向目标对象的原型
生成实例对象的常用方法是,使用new
命令让构造函数返回一个实例。但是很多时候,只能拿到一个实例对象,它可能根本不是由构建函数生成的,那么能不能从一个实例对象,生成另一个实例对象呢?
JavaScript 提供了Object.create
方法,用来满足这种需求。该方法接受一个对象作为参数,然后以它为原型,返回一个实例对象。该实例完全继承原型对象的属性。
1 | // 原型对象 |
上面代码中,Object.create
方法以A
对象为原型,生成了B
对象。B
继承了A
的所有属性和方法。
实际上,Object.create
方法可以用下面的代码代替。
内部实现原理
1 | if (typeof Object.create !== 'function') { |
上面代码表明,Object.create
方法的实质是新建一个空的构造函数F
,然后让F.prototype
属性指向参数对象obj
,最后返回一个F
的实例,从而实现让该实例继承obj
的属性。
下面三种方式生成的新对象是等价的。
1 | var obj1 = Object.create({}); |
如果想要生成一个不继承任何属性(比如没有toString
和valueOf
方法)的对象,可以将Object.create
的参数设为null
。
1 | var obj = Object.create(null); // 不继承Object的toString和valueOf方法的一个对象 |
上面代码中,对象obj
的原型是null
,它就不具备一些定义在Object.prototype
对象上面的属性,比如valueOf
方法。
使用Object.create
方法的时候,必须提供对象原型,即参数不能为空,或者不是对象,否则会报错。
1 | Object.create() |
Object.create
方法生成的新对象,动态继承了原型。在原型上添加或修改任何方法,会立刻反映在新对象之上。
1 | var obj1 = { p: 1 }; |
上面代码中,修改对象原型obj1
会影响到实例对象obj2
。
除了对象的原型,Object.create
方法还可以接受第二个参数。该参数是一个属性描述对象,它所描述的对象属性,会添加到实例对象,作为该对象自身的属性。
1 | var obj = Object.create({}, { |
Object.create
方法生成的对象,继承了它的原型对象的构造函数。
1 | function A() {} |
上面代码中,b
对象的原型是a
对象,因此继承了a
对象的构造函数A
。
4、Object.prototype.isPrototypeOf()判断某个对象是否为参数对象的原型
实例对象的isPrototypeOf
方法,用来判断该对象是否为参数对象的原型。
1 | var o1 = {}; |
上面代码中,o1
和o2
都是o3
的原型。这表明只要实例对象处在参数对象的原型链上,isPrototypeOf
方法都返回true
。
1 | Object.prototype.isPrototypeOf({}) // true |
上面代码中,由于Object.prototype
处于原型链的最顶端,所以对各种实例都返回true
,只有直接继承自null
的对象除外。
5、Object.prototype.__proto__ 返回该对象的原型,可读写
实例对象的__proto__
属性(前后各两个下划线),返回该对象的原型。该属性可读写。
1 | var obj = Object.create({x:1}) // 创建实例对象obj,其原型指定为{x:1} |
上面代码通过Object.create
创建实例对象obj,指定其原型为{x:1}
,访问obj对象的__proto__
属性,返回其原型。
1 | var obj = {}; |
上面代码通过__proto__
属性,将p
对象设为obj
对象的原型。
根据语言标准,__proto__
属性只有浏览器才需要部署,其他环境可以没有这个属性。它前后的两根下划线,表明它本质是一个内部属性,不应该对使用者暴露。因此,应该尽量少用这个属性,而是用Object.getPrototypeOf()
和Object.setPrototypeOf()
,进行原型对象的读写操作。
原型链可以用__proto__
很直观地表示。
1 | var A = { |
上面代码中,A
对象和B
对象的原型都是proto
对象,它们都共享proto
对象的print
方法。也就是说,A
和B
的print
方法,都是在调用proto
对象的print
方法。
6、获取原型对象方法的比较
如前所述,__proto__
属性指向当前对象的原型对象,即构造函数的prototype
属性。
1 | var obj = new Object(); |
上面代码首先新建了一个对象obj
,它的__proto__
属性,指向构造函数(Object
或obj.constructor
)的prototype
属性。
因此,获取实例对象obj
的原型对象,有三种方法。
obj.__proto__
obj.constructor.prototype
Object.getPrototypeOf(obj)
上面三种方法之中,前两种都不是很可靠。__proto__
属性只有浏览器才需要部署,其他环境可以不部署。而obj.constructor.prototype
在手动改变原型对象时,可能会失效。
1 | var P = function () {}; |
上面代码中,构造函数C
的原型对象被改成了p
,但是实例对象的c.constructor.prototype
却没有指向p
。所以,在改变原型对象时,一般要同时设置constructor
属性。
1 | C.prototype = p; |
因此,推荐使用第三种Object.getPrototypeOf
方法,获取原型对象。
7、Object.getOwnPropertyNames()
Object.getOwnPropertyNames
方法返回一个数组,成员是参数对象本身的所有属性的键名,不包含继承的属性键名。
1 | Object.getOwnPropertyNames(Date) |
上面代码中,Object.getOwnPropertyNames
方法返回Date
所有自身的属性名。
对象本身的属性之中,有的是可以遍历的(enumerable),有的是不可以遍历的。Object.getOwnPropertyNames
方法返回所有键名,不管是否可以遍历。只获取那些可以遍历的属性,使用Object.keys
方法。
1 | Object.keys(Date) // [] |
上面代码表明,Date
对象所有自身的属性,都是不可以遍历的。
8、Object.prototype.hasOwnProperty()
对象实例的hasOwnProperty
方法返回一个布尔值,用于判断某个属性定义在对象自身,还是定义在原型链上。
1 | Date.hasOwnProperty('length') // true |
上面代码表明,Date.length
(构造函数Date
可以接受多少个参数)是Date
自身的属性,Date.toString
是继承的属性。
另外,hasOwnProperty
方法是 JavaScript 之中唯一一个处理对象属性时,不会遍历原型链的方法。
9、in 运算符和 for…in 循环
in
运算符返回一个布尔值,表示一个对象是否具有某个属性。它不区分该属性是对象自身的属性,还是继承的属性。
1 | 'length' in Date // true |
in
运算符常用于检查一个属性是否存在。
获得对象的所有可遍历属性(不管是自身的还是继承的),可以使用for...in
循环。
1 | var o1 = { p1: 123 }; |
上面代码中,对象o2
的p2
属性是自身的,p1
属性是继承的。这两个属性都会被for...in
循环遍历。
为了在for...in
循环中获得对象自身的属性,可以采用hasOwnProperty
方法判断一下。
1 | for ( var name in object ) { |
获得对象的所有属性(不管是自身的还是继承的,也不管是否可枚举),可以使用下面的函数。
1 | function inheritedPropertyNames(obj) { |
上面代码依次获取obj
对象的每一级原型对象“自身”的属性,从而获取obj
对象的“所有”属性,不管是否可遍历。
下面是一个例子,列出Date
对象的所有属性。
1 | inheritedPropertyNames(Date) |
10、对象的拷贝
如果要拷贝一个对象,需要做到下面两件事情。
- 确保拷贝后的对象,与原对象具有同样的原型。
- 确保拷贝后的对象,与原对象具有同样的实例属性。
下面就是根据上面两点,实现的对象拷贝函数。
1 | function copyObject(orig) { // 拷贝对象函数 |
另一种更简单的写法,是利用 ES2017 才引入标准的Object.getOwnPropertyDescriptors
方法。
1 | function copyObject(orig) { |
五、严格模式
除了正常的运行模式,JavaScript 还有第二种运行模式:严格模式(strict mode)。顾名思义,这种模式采用更加严格的 JavaScript 语法。
同样的代码,在正常模式和严格模式中,可能会有不一样的运行结果。一些在正常模式下可以运行的语句,在严格模式下将不能运行。
1、设计目的
早期的 JavaScript 语言有很多设计不合理的地方,但是为了兼容以前的代码,又不能改变老的语法,只能不断添加新的语法,引导程序员使用新语法。
严格模式是从 ES5 进入标准的,主要目的有以下几个。
- 明确禁止一些不合理、不严谨的语法,减少 JavaScript 语言的一些怪异行为。
- 增加更多报错的场合,消除代码运行的一些不安全之处,保证代码运行的安全。
- 提高编译器效率,增加运行速度。
- 为未来新版本的 JavaScript 语法做好铺垫。
总之,严格模式体现了 JavaScript 更合理、更安全、更严谨的发展方向。
2、启用方法
1 | ; |