inheritance

文章目录

对象继承

Call 和 apply 和 bind

1. call 方法

语法:func.call(thisObj,arg1,arg2,arg3...)

定义:调用一个对象的一个方法,以另一个对象替换当前对象

说明:它运行一个方法,提供的第一个参数作为 this,后面的作为参数。如果没有提供 thisObj 参数,那么 Global 对象被用作 thisObj

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var name = "张三";
var obj = {
name: "李白"
}
function sayName(){
return this.name;
}

sayName(); // 张三
sayName.call(obj); // 李白

function sing(lyric){
return this.name + '在唱:'+ lyric;
}
sing("狼烟起江山北望");
sing.call(obj,"我在遥望,月亮之上")

新的检验类型的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var s = Object.prototype.toString;

s.call(123); // "[object Number]"
s.call('dsf') // "[object String]"
s.call({}) // "[object Object]"
s.call([]) // "[object Array]"
s.call(true) // "[object Boolean]"
s.call(undefined)// "[object Undefined]"
s.call(null) // "[object Null]"
s.call(NaN) // "[object Number]"
s.call(Infinity)// "[object Number]"
s.call(new Date())// "[object Date]"
s.call(Math) // "[object Math]"
s.call(JSON) // "[object JSON]"

2. apply 方法

语法:func.apply(thisObj,[argArray])

定义:应用某一对象的一个方法,以另一个对象替换当前对象,第二个参数是数组或类数组

说明:如果第二个参数不是数组或类数组,那么将导致一个 typeError。如果没有提供 thisObj,那么 Global 对象将被用作 thisObj,并且无法被传递任何参数。

1
2
3
4
5
6
7
8
9
var name = "张三";
var obj = { name : "李白" };
function sayFriend(f1,f2){
return this.name + '的朋友有:' + f1 + '、' + f2;
}

sayFriend("李四","王五");
sayFriend.call(obj,"杜甫","白居易");
sayFriend.apply(obj,['杜甫',"白居易"]);

callapply 之间唯一的语法区别是 call 接受一个参数列表,而 apply 则接受带有一个类似数组的对象。

但是当前可以使用 spread 运算符将数组作为参数列表传递,使用 call 模拟 apply

1
2
sayFriend.call(obj,...['杜甫',"白居易"])
// 但是这种使用方式没有 apply 解析速度快

apply 最重要的用途之一是将调用传递给另一个函数,如下所示:

1
2
3
let wrapper = function() {
return anotherFunction.apply(this, arguments);
};

这叫做 呼叫转移wrapper 传递它获得的所有内容:上下文 thisanotherFunction 的参数并返回其结果。

3. bind 方法

基础语法:

1
var boundFunc = func.bind(context);

func.bind(context) 的结果是一个特殊的像函数一样的“外来对象”,它可以像函数一样被调用并且透明的将调用传递给 func 并设置 this=context。即,调用 boundFunc 就像是调用 func 并且固定住了 this

例如:

1
2
3
4
5
6
7
8
9
10
var user = {
name: "John"
}

function func(){
console.log(this.name)
}

var funcUser = func.bind(user);
funcUser(); // John

偏函数

bind 的完整语法:

1
var bound = func.bind(context, arg1, arg2, ...);

绑定 this 以及函数的前几个参数。常用来制作偏函数应用(在原有函数的基础上创建出一个新函数,同时将部分参数换成固定值。例如:

1
2
3
4
5
6
7
// 创建一个乘法运算的函数
function mul(a,b){
return a * b;
}
// 在其基础上创建一个2倍函数
var double = mul.bind(null, 2);
console.log(double(3)); // = mul(2,3) = 6

情景:假如我们想要传递一些参数,但是不想绑定 this,该怎么做?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function partial(func, ...argsBound){
return function (...args){
return func.call(this, ...argsBound, ..args);
}
}

// 用法:
var user = {
firstName: "John",
say(time, phrase){
alert(`[${time}] ${this.firstName}: ${phrase}!`);
}
}

// 添加一个偏函数方法,现在 say 这个函数可以作为第一个函数
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());

user.sayNow("Hello");
// 结果就像这样:
// [10:00] John: Hello!

partial(func[, arg1, arg2...]) 调用的结果是一个基于 func 的封装函数,以及:

  • 和它传入的函数一致的 this (对于 user.sayNow 调用是 user)
  • 然后传入 ...argsBound —— 来自偏函数调用传入的参数("10:00"
  • 然后传入 ...args —— 传入封装函数的参数(Hello

柯里化

Currying 是一项将一个调用形式为 f(a,b,c) 的函数转化为调用形式为 f(a)(b)(c) 的技术。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function curry(func){
return function (a){
return function (b){
return func(a,b);
}
}
}

// 用法
function sum(a, b) {
return a + b;
}

var carriedSum = curry(sum);
console.log( carriedSum(1)(2) ); // 3

高级的柯里化同时允许函数正常调用和获取偏函数。从而实现两个目的:

  1. 柯里化之后我们没有丢失任何东西。
  2. 在很多情况下我们可以很方便生成偏函数。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
};
}

// 一个打印函数 log(date, importance, message) 格式化和输出信息。
function log(date, importance, message) {
alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`);
}
// 将它柯里化
log = curry(log);

// 操作之后 log 依然正常运行:
log(new Date(), "DEBUG", "some debug");

// 但是也可以用柯里化格式调用:
log(new Date())("DEBUG")("some debug"); // log(a)(b)(c)

// 创建一个获取今天的日志的简易函数:
// todayLog 会是一个首个参数确定的偏函数
var todayLog = log(new Date());
// 使用它
todayLog("INFO", "message"); // [HH:mm] INFO message

// 提供今天的调试信息的简便函数:
var todayDebug = todayLog("DEBUG");
todayDebug("message"); // [HH:mm] DEBUG message

小结

  • 当我们确定一个函数的一些参数时,返回的函数被称为偏函数。我们可以使用 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(){
this.name = '李白'
this.hobbies = ['吟诗','旅游']
}

Person.prototype.sayName = function(){
console.log(this.name)
}

function Student(){
this.age = "24"
}

let student = new Student()
console.log(student.age) // '24'
console.log(student.name) // undefined
student.sayName() // 报错

如果想让 student 访问到 Person的原型的对象的属性和方法,可以将 Student.prototype 改写为 Person 的实例

1
2
// 改写Student.prototype指针指向
Student.prototype = new Person();

这就是原型链继承

原型链的问题:

原型对象类似一个共享库,所有实例共享原型对象同一个属性方法,如果原型对象上有引用类型,那么会被所有实例共享,也就是某个实例更改了,则会影响其他实例,例如:

1
2
3
4
5
6
var student2 = new Student()
// 此时我们修改某一个实例,hobbies是原型对象上的引用类型 数组
student.hobbies.push('喝酒')

console.log(student.hobbies) // ['吟诗', '旅游', '喝酒']
console.log(student2.hobbies) // ['吟诗', '旅游', '喝酒']

从上面可以看出,student 的 hobbies(实际就是原型对象上的 hobbies)被修改后,相关的实例student2也会受到影响。

解决:把Person上的属性方法添加到Student上,以防都存在原型对象上,会被所有实例共享,特别是引用类型的修改,会影响所有相关实例。

借用构造函数继承

有时候也叫做伪造对象或经典继承,可以利用call来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function Person(){
this.name = '李白'
this.hobbies = ['吟诗','旅游']
}

Person.prototype.sayName = function(){
console.log(this.name)
}

function Student(){
Person.call(this) // 利用call调用Person上的属性方法拷贝一份到Student
this.age = 24
}

Student.prototype = new Person()

let student = new Student()
let student2 = new Student()
console.log(student.age) // 24
console.log(student.name) // '李白'
console.log(student.hobbies) // '['吟诗','旅游']'
student.sayName() // '李白'

// 此时我们修改某一个实例,hobbies 是原型对象上的引用类型 数组
student.hobbies.push('喝酒')

console.log(student.hobbies) // ['吟诗', '旅游', '喝酒']
console.log(student2.hobbies) // ['吟诗', '旅游']

上面在子构造函数(Student)中利用call调用父构造函数(Person)的方式,叫做借助构造函数继承

结合上面所看,使用了原型链继承和借助构造函数继承,两者结合起来使用叫组合继承,关系图如下:

1.jpg

那么还有个问题,当父构造函数需要接收参数时,怎么处理?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function Person(name,hobbies){ // 父构造函数接收name,hobbies参数
this.name = name // 赋值到this上
this.hobbies = hobbies // 赋值到this上
}

Person.prototype.sayName = function(){
console.log(this.name)
}

function Student(age,name,hobbies){ // 在子构造函数中也接收参数
Person.call(this,name,hobbies) // 在这里把name和hobbies传参数
this.age = age // 赋值到this上
}

Student.prototype = new Person()
Student.prototype.constructor = Student

let student = new Student(24,"李白",["吟诗","旅游"])
let student2 = new Student(22,"杜甫",["忧虑","想李白"])
console.log(student.age) // 24
console.log(student.name) // '李白'
console.log(student.pets) // '["吟诗","旅游"]'
student.sayName() // '李白'

student.hobbies.push('喝酒')

console.log(student.hobbies) // '["吟诗","旅游","喝酒"]'
console.log(student2.hobbies) // '["吟诗","旅游"]'

这样我们就可以在子构造函数中给父构造函数传参了,但是使用Person.call(this,name,hobbies)new Person()实例中的属性重复了,能否在子构造函数设置原型对象的时候,只要父构造函数的原型对象属性方法呢?当然是可以的。

寄生式组合继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function Person(name,hobbies){ // 父构造函数接收name,hobbies参数
this.name = name // 赋值到this上
this.hobbies = hobbies // 赋值到this上
}

Person.prototype.sayName = function(){
console.log(this.name)
}

function Student(age,name,hobbies){ // 在子构造函数中也接收参数
Person.call(this,name,hobbies) // 在这里把name和hobbies传参数
this.age = age // 赋值到this上
}

// 寄生式继承
function Temp(){} // 声明一个空的构造函数,用于桥梁作用
Temp.prototype = Person.prototype // 把Temp构造函数的原型对象指向Person的原型对象
let temp = new Temp() // 用构造函数Temp实例化一个实例temp
Student.prototype = temp // 把子构造函数的原型对象指向temp
temp.constructor = Student // 把temp的constructor指向Student

let student1 = new Student(24,'李白',['吟诗','旅游'])
console.log(student1) // Student { name: '李白', hobbies: [ '吟诗', '旅游' ], age: '24' }

let student2 = new Student(22,'杜甫',['忧虑'])
console.log(student2) // Student { name: '杜甫',hobbies: [ '忧虑' ], age: 22 }

至此为止,我们就完成了寄生式组合继承了,主要逻辑就是用一个空的构造函数,来当做桥梁,并且把其原型对象指向父构造函数的原型对象,并且实例化一个temp,temp会沿着这个原型链,去找到父构造函数的原型对象

2.jpg

原型式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 原型式继承
function createObjWithObj(obj){ // 传入一个原型对象
function Temp(){}
Temp.prototype = obj
let o = new Temp()
return o
}

// 把Person的原型对象当做temp的原型对象
let temp = createObjWithObj(Person.prototype)

// 也可以使用Object.create实现
// 把Person的原型对象当做temp2的原型对象
let temp2 = Object.create(Person.prototype)

寄生式继承

1
2
3
4
5
6
7
8
9
// 寄生式继承
// 我们在原型式的基础上,希望给这个对象新增一些属性方法
// 那么我们在原型式的基础上扩展
function createNewObjWithObj(obj) {
let o = createObjWithObj(obj)
o.name = "李白"
o.age = 28
return o
}
分享到:

评论完整模式加载中...如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理