对象继承
Call 和 apply 和 bind
1. call 方法
语法:func.call(thisObj,arg1,arg2,arg3...)
定义:调用一个对象的一个方法,以另一个对象替换当前对象
说明:它运行一个方法,提供的第一个参数作为 this
,后面的作为参数。如果没有提供 thisObj 参数,那么 Global 对象被用作 thisObj
例如:
1 | var name = "张三"; |
新的检验类型的方法:
1 | var s = Object.prototype.toString; |
2. apply 方法
语法:func.apply(thisObj,[argArray])
定义:应用某一对象的一个方法,以另一个对象替换当前对象,第二个参数是数组或类数组
说明:如果第二个参数不是数组或类数组,那么将导致一个 typeError。如果没有提供 thisObj,那么 Global 对象将被用作 thisObj,并且无法被传递任何参数。
1 | var name = "张三"; |
call
和 apply
之间唯一的语法区别是 call
接受一个参数列表,而 apply
则接受带有一个类似数组的对象。
但是当前可以使用 spread 运算符将数组作为参数列表传递,使用 call 模拟 apply
1 | sayFriend.call(obj,...['杜甫',"白居易"]) |
apply
最重要的用途之一是将调用传递给另一个函数,如下所示:
1 | let wrapper = function() { |
这叫做 呼叫转移。wrapper
传递它获得的所有内容:上下文 this
和 anotherFunction
的参数并返回其结果。
3. bind 方法
基础语法:
1 | var boundFunc = func.bind(context); |
func.bind(context)
的结果是一个特殊的像函数一样的“外来对象”,它可以像函数一样被调用并且透明的将调用传递给 func
并设置 this=context
。即,调用 boundFunc
就像是调用 func
并且固定住了 this
。
例如:
1 | var user = { |
偏函数
bind
的完整语法:
1 | var bound = func.bind(context, arg1, arg2, ...); |
绑定 this
以及函数的前几个参数。常用来制作偏函数应用(在原有函数的基础上创建出一个新函数,同时将部分参数换成固定值。例如:
1 | // 创建一个乘法运算的函数 |
情景:假如我们想要传递一些参数,但是不想绑定 this,该怎么做?
1 | function partial(func, ...argsBound){ |
partial(func[, arg1, arg2...])
调用的结果是一个基于 func
的封装函数,以及:
- 和它传入的函数一致的
this
(对于user.sayNow
调用是user
) - 然后传入
...argsBound
—— 来自偏函数调用传入的参数("10:00"
) - 然后传入
...args
—— 传入封装函数的参数(Hello
)
柯里化
Currying 是一项将一个调用形式为 f(a,b,c)
的函数转化为调用形式为 f(a)(b)(c)
的技术。
例如:
1 | function curry(func){ |
高级的柯里化同时允许函数正常调用和获取偏函数。从而实现两个目的:
- 柯里化之后我们没有丢失任何东西。
- 在很多情况下我们可以很方便生成偏函数。
例如:
1 | function curry(func) { |
小结
当我们确定一个函数的一些参数时,返回的函数被称为偏函数。我们可以使用
bind
来获取偏函数,但是也有其他方式获取。当我们不想一遍又一遍重复相同的参数时,偏函数很方便。比如我们有函数
send(from, to)
,并且在我们的任务中from
始终是相同的,那么我们可以构造一个偏函数然后对它进行操作。柯里化是将
f(a,b,c)
可以被以f(a)(b)(c)
的形式被调用的转化。JavaScript 实现版本通常保留函数被正常调用和在参数数量不够的情况下返回偏函数这两个特性。当我们想要简单偏函数的时候,柯里化很棒。正如我们在 logging 例子中所看到的那样:通用函数
log(date, importance, message)
在柯里化之后,当我们在调用它的时候传入一个参数如log(date)
或者两个参数log(date, importance)
的时候,返回了偏函数。
对象继承
继承定义:继承可以使子类具有父类的各种属性和方法,而不需要再次编写相同的代码
面向对象的继承方式有很多种,原型链继承、借用构造函数继承、组合继承、原型式继承、寄生式继承、寄生式组合继承、深拷贝继承等等。
原型链继承
利用原型链的特性,当在自身找不到时,会沿着原型链往上找。
1 | function Person(){ |
如果想让 student 访问到 Person的原型的对象的属性和方法,可以将 Student.prototype 改写为 Person 的实例
1 | // 改写Student.prototype指针指向 |
这就是原型链继承
原型链的问题:
原型对象类似一个共享库,所有实例共享原型对象同一个属性方法,如果原型对象上有引用类型,那么会被所有实例共享,也就是某个实例更改了,则会影响其他实例,例如:
1 | var student2 = new Student() |
从上面可以看出,student 的 hobbies(实际就是原型对象上的 hobbies)被修改后,相关的实例student2也会受到影响。
解决:把Person上的属性方法添加到Student上,以防都存在原型对象上,会被所有实例共享,特别是引用类型的修改,会影响所有相关实例。
借用构造函数继承
有时候也叫做伪造对象或经典继承,可以利用call来实现。
1 | function Person(){ |
上面在子构造函数(Student)中利用call调用父构造函数(Person)的方式,叫做借助构造函数继承
结合上面所看,使用了原型链继承和借助构造函数继承,两者结合起来使用叫组合继承,关系图如下:
那么还有个问题,当父构造函数需要接收参数时,怎么处理?
1 | function Person(name,hobbies){ // 父构造函数接收name,hobbies参数 |
这样我们就可以在子构造函数中给父构造函数传参了,但是使用Person.call(this,name,hobbies)
和new Person()
实例中的属性重复了,能否在子构造函数设置原型对象的时候,只要父构造函数的原型对象属性方法呢?当然是可以的。
寄生式组合继承
1 | function Person(name,hobbies){ // 父构造函数接收name,hobbies参数 |
至此为止,我们就完成了寄生式组合继承了,主要逻辑就是用一个空的构造函数,来当做桥梁,并且把其原型对象指向父构造函数的原型对象,并且实例化一个temp,temp会沿着这个原型链,去找到父构造函数的原型对象
原型式继承
1 | // 原型式继承 |
寄生式继承
1 | // 寄生式继承 |