#JavaScript第五次记录整理
###关于上次函数的复习
对象的属性也可以是一个函数,比如 var a = {b: 9, c: function () {} };
这里 a.c
就是一个函数,对象的函数称为方法。其实没什么特殊的,只不过是一个属性而已.
不过呢,JS语言规定,在以 a.c()
这种形式调用函数时,在函数c
调用过程中设置 this
变量为 a
.
形式就是[obj].[prop]()
的调用,会设置 this
为 [obj]
其他以外和一般的函数没有任何区别.
利用这个特性,函数就可以知道调用自己在哪个对象上调用.
例子: var a = {b: 9, c: function (d) { return this.b + d; } };
这样的话 a.c(3)
返回值就是 12
这里 this
被设置为 a
.
如果有很多很多对象,都有相同的函数,要怎么设置呢?
这些函数除了 this 不同,其他都相同,也就是说函数本身都是一样的.
一种方法当然是把函数放到一个变量里,然后再分别用在每个对象上.
比如 var sayHello = function () { console.log("Hello from " + this.name) };
然后就可以 var alice = {name: "Alice", sayHello: sayHello}; var bob = {name: "Bob", sayHello: sayHello};
不过这样写起来太繁琐了。假如有一类对象都需要 sayHello 函数,那要写好久好久的.
###prototype
先来讲 prototype
.JS 有一个叫做 prototype
的概念.
在查找某个属性的时候,如果这个对象上没有这个属性,则会到原型上继续查找.
比如 alice.sayHello
没有的情况下,就会取 alice
的原型上的 sayHello
.
这个所谓原型,是一个隐藏属性,不能直接通过取属性来得到.
但是,原型的存在却使得原本应该取不到任何值的属性,变成去原型上取属性.
好吧,那么我们如何使用原型呢?
先来介绍 Object.create
这个函数的作用是以某个对象为原型,创建一个新的对象.比如 var b = Object.create(a);
,那么返回的对象 b
的原型就是 a
.
有了这个以后,我们可以这样做:
var person = {sayHello: function () { return this.name; }};
var alice = Object.create(person); alice.name = "Alice"
这样 alice.sayHello
就会从原型 person
上取得 sayHello
属性了.于是 alice.sayHello()
就能够成立.
要注意的是,该对象上取某个属性的时候,该属性不存在,才会找原型上的属性.如果 alice.sayHello
存在,那么就不会继续找下去了.
对象的属性可以覆盖原型的属性.特别注意一下原型查找的条件只对取属性成立,存属性(赋值)并不成立.
因此,原型并不能允许共享可修改的属性,而一般只用来共享一些函数什么的.
原型也可以有自己的原型,这样依次查找上去,形成原型链.
对象的预设属性有些可以修改,有些不能.原型也是一个对象,所以上面可以放很多属性.
如果不是由 Object.create
创建的对象,比如 {} ,它的原型是 Object.prototype
, 这个对象.
而 Object.prototype 没有原型(或者说原型是 null)原型链就是这样形成的.
刚才的例子: var person = {sayHello: function () { return this.name; }};
var alice = Object.create(person);,
这种情况下, 取 alice.sayHello
需要查找原型链 大致顺序就是 alice -> person -> Object.prototype -> null
其中查找到 person
就找到了,所以不再向上查找了.
原型和传统面向对象的最大的区别是,对象继承对象,而不是对象的类继承另一个类.
原型是一种联系关系(隐藏属性),而不是简单的复制所有属性过来.
person
也可以进行修改或者增加新属性,可以反映到 alice
上.
用 Object.getPrototypeOf(a);
可以获取 a
的原型.
这个例子中 Object.getPrototypeOf(alice)
会是 person
.
有了这些以后,我们可以创建这样一个原型结构:鸡是一种鸟,而鸟是一种动物
可以是 var animal= { ... }; var bird = Object.create(animal); var chicken = Object.create(bird);
通过修改 Object.prototype
,可以给所有对象增加属性,包括那些直接写 {}
的
但是那个是不推荐的 因为会有太多东西受影响了 而且 Object.prototype
只有一个,所以可能会影响到不只是自己的代码,还有使用的库的代码,等等
###小练习
下面是一个小练习,通过 Object.create
创建10个不同的人,有不同的 firstName 和 lastName 。而原型上需要有 getFullName 方法(函数),用来获取全名(连在一起),需要在循环语句中使用 Object.create
###构造函数
JS提供了另一种很方便地创建对象的方法,叫构造函数.
写法是 var Person = function () { this.sayHello = ...; this.name = "Amy"; }
.
可以看到构造函数首先是一个函数,而且构造函数中可以用 this 给正在创建的对象增加属性等.
调用方法是 var amy = new Person();
与一般函数调用类似,只是多一个关键字 new
。这个关键字 new
改变了构造函数中 this
的含义.
构造函数调用时,创建一个空对象,并且在构造函数调用过程中 this
变成那个对象。之后返回新创建的对象.
构造函数的一个好处就是可以批量给对象赋值.
任何用 new Person()
创建出的对象都经历过 Person
构造函数中的初始化.
比 var amy = {}; Person(amy);
之类的要方便.
构造函数还有一个作用,就是自动设置 prototype.
一个对象只能用一个构造函数创建.
从语法就可以看出 new Person()
不允许多个构造函数的名字.
如果是重载的话, JS 和很多动态语言一样,没有重载的.
不过可以自己判断参数来实现不同功能.
每个构造函数自带有一个 prototype
,如 Person.prototype
.
然后呢,创建 new Person
的过程中,新创建对象的原型自动设置为 Person.prototype
于是,我们就可以 Person.prototype.sayHello = function () { ... };
用这样的方法,所有 new Person
创建的对象都可以从 Person.prototype
上获取到 sayHello
当然原理还是之前说过的原型链,这里就不多赘述了.
这种创建对象的方式(构造函数方式),常常和其他语言的类来相比.
这里 Person
可以看作是类.
new
后面接受一个函数(指向函数的变量)
我们通常使用这样的命名习惯,只要一个函数首字母是大写,就认为这个函数应该使用 new
来使用。如果第一个字母是小写则认为是不用作构造函数.
这只是众多风格之一,但是这里想要推荐这种风格.
因为一个构造函数不用 new
调用,就会变成一般函数,产生奇怪的后果
一个函数是否是构造函数,区别仅仅在于调用时是否增加了 new
用构造函数怎么实现继承呢?很简单,把 Person.prototype
设置成一个其他对象就好
比如, Person.prototype = Object.create(animal)
为什么不直接 Person.prototype = animal
?因为那样就没办法给 Person.prototype
单独增加属性了.
往往来说,一旦我们开始用构造函数,那么我们就会到处用构造函数。
可以想像 Animal
和 Person
都是构造函数.
那么 Person.prototype = new Animal()
原型是一个对象,原型不是构造函数
不过这样的话, Animal
的构造函数必须允许构造一个空的,或者说非法的 new Animal()
对象,所以写构造函数的时候可能还没办法检查参数是否为空.
###instanceof
这个操作符,用法是 alice instanceof Person
作用就是,检查某个对象是否是通过某个构造函数创建的.
不过有一个特殊之处,为了达到类似继承的效果,这个操作符也会去翻 Person.prototype
的构造函数,之类的一直找下去.
对于 alice
继承关系上的每一个构造函数 C
, alice instanceof C
都返回 true
,其他为 false
因为 Person.prototype
默认是一个对象,而对象的构造函数是 Object
,所以 任何对象 instaceof Object
返回都是 true
alice instanceof Person; // --> true
alice instanceof Object; // --> true
但少部分对象,比如 Object.prototype
并不是 instaceof Object
,否则就死循环了……
这也是另一个构造函数可以与“类”类比的地方
创建一个对象使用的构造函数可以使用 obj.constructor
属性来获取.不过注意的是,这个属性仅仅是反映了 obj
的构造函数而已,并没有什么特殊的地方
obj.constructor
这个属性可以修改的,不过修改这个属性并不会真的修改 obj
的构造函数
构造函数,像原型一样,也是一个隐藏属性,只不过 .constructor
的初始值碰巧和那个隐藏属性一样
总结一下。某个对象的原型是一个隐藏属性,决定了不存在的属性的下一步查找顺序,也决定了 Object.getPrototypeOf(a)
的返回值
某个对象的构造函数是一个隐藏属性,决定了 .constructor
属性的初始值,决定 instanceof
的返回结果(true
或 false
)。不用构造函数创建的对象,默认 constructor
是 Object
用构造函数创建的对象,其原型隐藏属性被设置为构造函数的 .prototype
属性
这些说法是语法上正确规范的
那么关于构造函数和原型就介绍到这里吧
知道了构造函数以后,我们就可以去试试看一些基本对象的构造函数是什么了
"abc".constructor
是 String
([]).constructor
是 Array
9..constructor
或者 (9).constructor
是 Number
9.constructor
是语法错误
,因为据说……解释器会把那个点当做小数点吃掉,然后就不吐出来了
这些都是构造函数,至于怎么用,有哪些附加功能,大家可以自己去看相关资料
大家要明白对象的本质是“身份”,也就是不相等性。 像 {} !== {}
,但 9 === 9
,所以数不具有身份
另外, var b = 9;
然后 b.prop = "Hello";
是不能添加属性的
只是能执行而已,之后 b.prop
返回还是 undefined
这个就是数、字符串与对象的本质区别了
###this
this
在JS中,是一个特殊的变量,和调用有关,而和语法结构无关
this
不像一般的变量一样。this
不受闭包影响
每次函数调用的时候, this
的值可能更改也可能不更改。调用完毕后, this
的值还原
首先我们解释一下,在全局范围内, this
的初始值是全局对象,比如 window
普通的函数调用,如 a();
不改变 this
的值,也就是说 a
执行范围内的 this
和外部相同.
形式是 xxx.yyy()
的调用,在调用期间 this
设置为 xxx
,调用完毕后还原.
也就是说,在 yyy
函数执行的时候 this
是 xxx
在 new F()
期间, this
设置成新创建的对象(Object.create(F.prototype)
),调用完毕还原
可以看到, this
的值依赖于调用的嵌套关系(或者说堆栈),并且依赖于调用的形式(普通调用、方法调用、构造函数调用),与定义的位置无关
环境提供的代码执行期间,允许任意更改 this
的值,只要返回的时候还原就可以
一个典型的例子就是 DOM
的事件处理函数,如 element.onclick = function () {};
在 element.onclick
函数调用期间, onclick
被设置成了此事件的当前元素,一般来说是 element
原因就是环境
所以说,在嵌套函数的时候,千万不要假设里面的 this
还是外面的 this
为了保持 this
的值,通常选择使用一个变量 var that = this;
然后在里面的函数里使用 that
也有用 self
等名字的。这名字只是一个变量而已
接下去要讲和 this
相关的几个函数, bind
, call
和 apply
首先 var func = function () {}
,然后 var boundFunc = func.bind(alice);
那么 boundFunc
是一个新的函数,这个函数调用 func
,但是调用期间 this
被固定设置成 alice
这个对象
也就是说, var func = (function () { .... }).bind(this)
就可以确保 ...
执行的时候 this
不变
call
和 apply
提供的则是函数的另一种调用方法
a.call(alice, bob, cindy)
; 相当于 a(bob, cindy)
,唯一的区别就是 this
设置成 alice
如果在一个构造函数里需要调用另一个构造函数(比如完成“基类”的初始化),那么可以用 .call
来保证 this
不变
一个构造函数里调用另一个构造函数,显然再用 new
不行,因为那样就不是同一个对象了
a.apply
是一个接受数组作为参数的 call
变种。 a.apply(alice, [bob, cindy])
相当于 a.call(alice, bob, cindy)
注意到第二个参数是一个数组,然后每一个数组的元素成为一个参数
这个可以用来做这样的事情: b.apply(this, arguments);
这样的话,就可以把一个函数的所有参数原封不动地用来调用另一个函数
有了这些以后,我们可以自己实现 bind : function (original, self) { return function () { return original.apply(self, arguments); } }
这个也可以用作 IE
的 polyfill
JS 没有提供改变 this
的任何办法,除了调用另一个函数以外
this = 9;
是不行的
然后,调用结束是自动复原的,请放心
##现在要布置一个最重要的作业了!
###JS 课程快要结束了……大家应该开始考虑下一步的发展方向了
之后应该可以有浏览器方向,NodeJS方向
请大家到环聊讨论啦~