全局作用域
- 一个js文件即使没有任何代码也存在一个全局作用域。
- 全局作用域被多个不同的文件共享,使得程序的任意部分都可以和另一个部分交互。
执行环境(内存作用域)
- 当程序运行时,会创建一个保存变量和变量值的存储系统,这些内存中的作用域结构就被称为执行环境。执行环境是在代码运行时才被创建的,而不是在代码输入时。
函数和执行环境
程序运行时,将会创建内部数据存储以记录可供不同函数对象访问的所有变量。
由于函数的每一次运行的时候都是完全独立于之前的每次运行的,每次函数运行的时候就会创建一个新的运行环境。因此对于每一个词法作用域而言,在运行中可能会创建多个内存作用域,也可能一个都没有。完全取决于你代码中的这个函数运行了多少次。
为了检查执行环境,我们需要运行这个程序。
闭包
简单地说,每个函数可以访问包围它的作用域中的所有变量,闭包就是在外部作用域都已返回之后,仍然可以被访问的任意函数。一个函数的环境用用都会被创建为它被定义的环境的子环境。
this的作用
关键词 this 使得我们可以仅创建一个函数对象,就可以将其作为方法用在一些其他的对象上。每次我们调用该方法时,它便可以访问任意使用它的对象。这对于保存内存非常有用。而这是因为我们能够访问参数this才能实现。
this 关键字
this 是个参数,因为它几乎表现得和普通参数一模一样,普通参数和参数this之间只有两个区别:
1. 你无法为参数this命名
2. 绑定参数this的方式
this是一个绑定值的标识符 **(this is an identifier that gets a value bound to it.much like a variable) **, 但它并不是明确地标识你代码中的值,而是自动绑定到正确的对象。一般来说,解释器如何决定正确绑定的规则,类似定位函数参数的规则。定位函数参数和关键字this之间的区别,是用于帮助你在调用方法或构造函数时确定哪个对象是焦点。
obj.fn(3,4) ** 在一个函数调用的左边有一个点符号的时候,就意味着它是作为一个对象的属性被查找的**。你可以看这个点符号的左边,从而得知它是在哪个对象中被查找的。在这个函数被调用时,用于查找它的这个对象,就是关键词this绑定的内容。
fn.call
通过使用函数方法.call,我们可以重写默认的全局对象绑定以及点符号左侧的规定。r.method.call(y,g,b)
使用.call 会重写方法访问规则。记住,在没有看到一个函数的具体调用之前,你是无法知道它的参数将会绑定什么值的。
回调函数中,为避免将参数绑定复杂化, 可以采取传入另一种函数的方法,这个函数完全不接受任何参数,包括this.
原型链
原型链是创建相似对象的一种机制。
当你为了节省内存或避免重复代码时,需要两个对象拥有完全相同的属性时,你可能需要从一个对象复制所有的属性到另一个对象。但JavaScript提供了原型链这种方式.** 通过将在第一个对象中的字段查找委派于第二个对象的方式,可以使得一个对象表现出完全拥有另一个对象的所有属性**。
函数Object.create()可以帮助你创建一个拥有这个委托查找功能的对象。
对象的属性中,非常有用的一个是 .constructor。可用于确认创建对象时使用了哪个函数。和其他的属性一样,.constructor 事实上是指向存储在别处的另一个对象。人们常常混淆对象原型和这个用于创建所有对象的构造函数。
函数类
类是一种可用于创建大量相似对象的强大的函数形式。
装饰器代码和类
装饰器代码和类之间的唯一区别是:类创建这个需要增加的对象(从无到有),而装饰器将需要增加的对象接受作为输入参数(把对方作为参数传递给方法,做一些处理后再返回)。比如:
装饰器:
类(构造器,在这里我们没有传入obj)
构造函数
类是一个构造器,它能够创建大量遵从大致相同模式的相似对象,创造这些大量相似对象的函数被称为构造函数,因为它的工作就是构造可以成为类的成员的对象。也就是说,类是一种你要创建的某一类实物,然而构造器仅仅是用于创建这个类的一个新的实例。(虽然我们前面基本提到类的时候就等价于构造器,但如果到语义层面来理解的话,两个还是不一样的,构造器 可以看出是实现类的一个手段)。
调用构造器函数返回的对象就是类的实例。
减少重复性(why prototype?)
上面这段代码在每次调用构造函数 Car 时都会创建一个全新的 move 函数,我们更希望能够只有一个 move 函数对象被所有的 Car 对象共享以节省内存。
像下面这样:
但这样有一个问题,这个函数无法访问变量obj。那怎么写呢?答案是使用参数this来实现:
参数 this 会将调用时符号左侧的对象当做函数输入参数,从而提供我们引用这个对象时可使用的名称。
优化上面的代码
以上的代码里我们发现有两个地方有涉及到move的命名。如果说我们有多个函数,像下面这样,那么就有一旦函数名改了,我们就需要同时改两个地方。
是否有方法可以让我们遍历想要添加到每个车辆对象上的所有方法,并将它们从某处的列表中自动添加到对象上?我们可能没有办法在一个作用域中遍历所有的变量,那么换个角度,也许我们可以将计划添加到车辆对象里的所有方法在一开始就进行存储,这样就可以轻松地通过程序化的方式遍历对象,并通过一行代码来添加他们。如下:
注意上面这种代码的命名方式,目前的方式无法明确表现出方法对象和类Car之间的从属关系,很容易将其误认为是其他某个类的方法容器,并且在目前的代码中,它还是一个全局变量。更好的方法是将对象清晰地封装在类Car函数中,这样它们就很工整地捆绑在了一起。这个其实很容易做到,我们可以利用函数 Car 的对象特性腾出空间以存储这些方法。(函数都是对象,对象可以有属性,函数当然也可以啦)如下:
属性访问权限
很多人在这个地方会有疑问。因为他们忘记了函数只是专门化的对象,除了可以被调用外,函数还可以像其他对象一样存储属性。想象变 Car 存储的是一个对象,而非一个函数,可以更容易想明白。
这里要指出的是,函数调用只会导致这其中的代码执行,调用函数并不会跟该函数属性发生任何交互。你可能会觉得因为我们向函数 Car 中添加了一个属性,可能会启动某些有趣的机制,或激活某些函数运行规则。但是我们向函数Car添加 .methods 属性的行为并不会导致任何有趣的规则生效。这只是一个简单的属性访问,如同往常一样,但这次的目的是将这些对象从全局作用域中移出并整理, Car.menthods 并没有任何特别之处,它被便利地隐藏为Car的属性。
原型类
这里我们实现相似对象的方法是保证它用同一个构造函数来实例化,这个构造函数里包含了所有需要被继承的属性和方法。我们要公用methods,我们就将这个方法通过extend复制到构造函数中。
能不能不适用extend来复制所有方法呢?
我们可以使用原型对象来存储所有共享方法。并使得实例委托到这个共享原型对象。回到我们的代码上,我们可以创建 amy 和 ben,并使他们委托到 Car.method,而不是将所有的Car.methods属性都复制到amy和ben中。
如何用原型链原理来实现上面这段代码的功能呢?
回忆一下原型链的原理:当对一个对象进行属性字段查找时,可以到另一个对象中继续查找。
首先,像上面这样使用对象字面量创建对象 Car 时,无法定义这个新对象的原型。所以我们需要变化一下,使用另一种创建对象的方式。** object.create()**,这种方式可以设置新对象的原型
那么object.create中应该放置什么对象?答案是car.methods
当实例中属性查找失败时,我们可以继续在car.mehods中查找。因此每个实例都会拥有方法.move.
这便是这个类的原型模式
那么这个方法和之前的复制有什么区别呢?
这是一个重要的转变,因为它让我们避免了在创建一个新的对象时要复制所有属性这个开销很大的步骤。
以这种原型模式构造类的过程是十分清晰的,你需要的仅仅是
- 一个让你构造实例的函数
- 一行使用那个函数生成实例对象的代码
- 一个从新对象到原型关系的委托
- 以及一些用属性来扩充对象的逻辑,使得这个对象不同于使用相同类构造的其他对象
因为这个模式非常常见,语言设计者决定增加官方约定来支持它。这就是** prototype**的前身历史!
在这里我们把 methods 作为 Car 函数对象的一个属性,如果我们计划在我们所有的类中使用原型,我们每次都很可能重复之前我们所做的一切。
因为建立一个支持方法的对象并把它作为属性绑定到一个构造函数是如此的普遍,语言本身会自动帮你做这件事。无论什么时候,当一个函数对象被创建时,都会被附加一个对象属性,你可以把它当做一个方法的容器。你可以把它当做一个方法的容器,用来处理你把这个函数当做构建实例的类。
我们在这里写的 methods 并不是语言本身为我们创建的那个容器,尽管之前我们选择key.methods作为我们的方法容器对象,但伴随每个函数自动生成的方法容器对象实际上是 key.prototype . 这是一个命名上的选择。
于是我们的代码就变成如下:
这里声明一下,使用 key.prototype 而非 key.methods 并没有声明实际意义,只是一个名称上的不同而已。
prototype的模糊性
原型这个词用来描述每个函数可用的方法容器对象。
如果有人说,对象A的原型是对象B,合理的解读应该是对象A的字段查找会被委托到对象B。所以,你可以说,amy 的原型是Car.prototype. 但是Car和Car.prototype之间却不是这种关系。在这里,Car是一个函数对象,它的字段查找会被委托到所有函数对象的字段查找都被委托到的某个函数原型。函数Car和Car.prototype之间的关系与 amy和Car.prototype之间的非常不同,他们之间的关系是 “ 对象A的原型是对象B ” 的第二种解读。这种关系是,当函数Car运行时,它将创建对象,这些对象会将他们的字段查找委托到Car.prototype.因此,从这个层面上,你可以说Car的原型是Car.prototype.
每个.prototype对象都拥有一个.constructor属性,指回附属于的函数。Car.prototype.constructor 就是Car本身。
这个特性的主要作用是用于判断是哪个构造函数创建了某个对象。
一个类的所有实例都将字段查找委托到它们的原型。因此它们拥有相同的构造函数。除此之外,运算符 instanceof 用于检查右侧运算符对象的.prototype对象是否存在在左侧运算符对象的原型链中。
log(amy instanceof Car) // return true
这里Car.prototype可以在amy的原型链中被找到,
** 伪类模型**
按照之前的说法,以上这两行代码看起来似乎会出现在每一个原型类中。那么优化的机会就出现了。
构造函数模式
** 为了减少这些输入,JavaScript提供了关键字 new 。无论什么时候我们选择在一个函数调用前面使用关键字new,我们的函数都会以一种叫做构造函数**的特殊模式来运行。以那种模式,我们可以期待JavaScript为我们自动化完成这些工作。
那么构造模式到底是什么?
基本而言,这是一种解释器在你的代码里嵌入一些操作的方式。因为它知道你需要这些会被自动完成。
无论什么时候你初始化一个新对象,它暂时使的你的函数运行仿佛这里有一些额外的代码在开始和结尾,即使你从来没有输入这些代码。这些被插入的操作做着像你之前在你的原型类里写的那些代码一样的工作。
以下便是我们所期望的构造伪类(pseudo-classical version)的方式。