秋招复习计划-JavaScript知识点2

  |  

前言

这部分复习的内容包括:bind、call、apply的区别,继承的实现方法及其比较,prototype和__proto__的关系及区别,new的原理


24



bind、call、apply的区别

定义

bind的定义:

func.bind(thisArg, arg1, arg2, ....])

bind方法返回一个绑定函数,thisArg当作绑定函数被调用时,该参数会作为原函数运行时的this指向。当使用new 操作符调用绑定函数时,thisArg会失效。arg1,arg2, ….当绑定函数被调用时,这些参数将置于实参之前传递给被绑定函数。

call的定义:

func.call(thisArg, arg1, arg2, ...)

thisArg为在func函数运行时指定的this值,需要注意的是,指定的this值并不一定是该函数执行时正真的this值。在非严格模式下,当this值为null或undefined时,函数执行的this指向全局对象,this值为原始值时,执行的this指向原始值的自动包装对象

arg1,arg2, …为指定的参数列表

apply的定义:

func.apply(thisArg, [argsArray])

hisArg为在func函数运行时指定的this值,需要注意的是,指定的this值并不一定是该函数执行时正真的this值。在非严格模式下,当this值为null或undefined时,函数执行的this指向全局对象,this值为原始值时,执行的this指向原始值的自动包装对象

argsArray是一个数组或类数组对象(ES5以后),其中的数组元素将作为单独的参数传递给func函数,如果这个参数为null或undefined,则表明不需要传入参数

区别

bind、call、apply三者都可以用于改变函数体内this的指向。call和apply的区别只在于参数。

apply和call的一个参数都是需要指向的this,apply的第二个参数是一个参数数组,而call的第二个及其以后的参数都是参数的列举,也就是说call函数的参数需要一一列举

bind与apply和call的区别在于,bind并不立即调用,而是返回一个新函数,称为绑定函数,其内的this指向为创建它是传入bind的第一个参数,而传入bind的第二个及以后的参数作为原函数的参数来调用原函数。

绑定函数不可以再通过apply和call改变其this的指向。

继承的实现及其比较

原型链继承

原型链继承是JS最基础的继承模式,每个构造函数都有一个原型对象,原型对象包含一个constructor属性,指向构造函数,而每个构造函数的实例都包含一个指向原型对象的内部指针(__proto__)。

原型链继承的基本方法是让子类型的原型对象作为要继承的父类型的实例,由于父类型的实例包含一个指向父类型构造函数的原型对象的内部指针,一次类推可以构成基于原型的链型结构,及原型链

代码实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
}
function SubType() {
this.subproperty = false;
}
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function() {
return this.subproperty;
}
let instance = new SubType();

在这个示例中,SubType继承于SupType,作为SubType的实例instance的__proto__指向SupType的prototype,而SubType的实例作为SubType的原型对象,这个原型对象的__proto__指向Object.prototype。

所以这个最终的原型链构成是这样的

1
2
3
instance.__proto__ -> SupType实例对象
SupType实例对象.__proto__ -> SupType构造函数的prototype
SupType构造函数的prototype对象.__proto__ -> Object.prototype

默认原型:

所有函数的默认原型都是Object的实例,这并没有任何的问题,因为原型本身就是一个对象,所以默认原型会继承例如toString()和valueOf()等默认方法

确定原型和实例的方法:

instance instanceOf Object

Object.prototype.isPrototypeOf(instance)

注意点:

给原型添加方法一定要放在替换原型语句之后,不然在原型被替换之后,给原先原型添加的方法将被覆盖

在通过原型链实现那个继承时,不能使用对象字面量的形式来创建对象方法,只能使用obj.prototype.方法名的形式添加

原型链继承的缺陷:

子类型的所有实例都可以共享父类型的原型属性和方法

子类型的实例无法在不影响所有对象的情况下,给父类型的构造函数传递参数。

对于这一句话的理解,我的认为是当构造函数将参数传递进父类型的原型时,由于所有的子类型实例都会继承父类型的原型属性和方法,那么当一个实例给父类型传递参数,而参数被用于原型时,所有实例继承自父类型的属性和方法都会跟着改变

借用构造函数继承

借用构造函数继承的模式又成为伪造对象或经典继承

这个模式的基本思想是在子类型的构造函数内不调用父类型构造函数(通过cal或apply强制绑定this的方式)来创建新的构造函数

代码实例

1
2
3
4
5
6
7
function SupType() {
this.value = "test";
}
function SubType() {
SupType.call(this);
}
let obj = new SubType();

借用构造函数继承有个重大的缺陷,及子类无法继承父类原型中的方法和属性,类型所有的方法都需要在构造函数中定义,这样会导致大量的冗余,函数复用将变得没有任何意义

组合继承

组合继承又称伪经典继承,是将原型链和借用构造函数技术组合的一种继承方式

其基本思想是,使用原型链事项对原型属性和方法的继承,而通过借用构造和拿书来属性对实例属性的继承(子类型的实例内部存在同名属性,从而屏蔽在子类原型上的父类型的同名属性)

代码实例

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
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
alert(this.age);
}
var instance1 = new SubType("Jack", 29);
instance1.colors.push("black");
console.log(instance1.colors);
instance1.sayName();
instance1.sayAge();
var instance2 = new SubType("Greg", 27);
console.log(instance2.colors);
instance2.sayName();
instance2.sayAge();

这个模式的缺点在于调用了两次父类构造函数,一次在创建子类型原型时一次在子类构造函数内部。这样会导致在子类和子类的原型上都有父类型的构造函数内的属性和方法,只不过子类内部的属性和方法会屏蔽子类原型上的。

原型式继承

注意原型式继承与原型链继承是有区别的

原型式继承的基本思想是借助原型可以基于已有对象创建新的对象。即如在一个函数Object内部,先创建一个临时的构造函数,然后将传入的对象作为这个构造函数的原型(本质上是浅复制),最后返回这个临时类型的新实例

代码实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function object(o) {
function F(){}
F.prototype = o;
return new F();
}
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
}
var anotherPerson1 = object(person);
anotherPerson1.name = "Greg";
anotherPerson1.friends.push("Rob");
var anotherPerson2 = object(person);
console.log(anotherPerson2.name);
console.log(anotherPerson2.friends);

anotherPerson1对name的直接赋值会屏蔽原型上的name属性,而对friends的操作会直接影响到原型的值,所以后续的anotherPerson2实例的friends是包含Rob元素的。这样也是它的一个缺陷。

ES5新增的Object.create()方法拥有与object()方法相同的效果,但是Object.create()方法可以接受两个参数:要给用作新对象原型的对象和一个为新对象定义的额外属性(会覆盖新对象原型的对象上的同名属性)

寄生式继承

创建一个仅用于封装继承过程的函数,该函数 在内部一某种形式来做增强对象,最后返回对象

代码实例

1
2
3
4
5
6
7
function createObj(o) {
let clone = object.create(o);
clone.sayName = function() {
console.log('hi');
}
return clone;
}

缺点:跟借用构造函数模式一样,每次创建对象都会创建一遍方法,而且这种继承会影响到父类型的方法

寄生组合式继承

之前的组合式继承最大的缺点给就是会调用两次父构造函数,而寄生组合式继承则避免了这个缺点,它让子类型的原型间接访问到父类型的原型。

代码实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function() {
console.log(this.name);
}
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
function inheritPrototype(child, parent) {
let prototype = Object.create(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
inheritPrototype(Child, Parent);
Child.prototype.getAge = function() {
console.log(this.age);
}

寄生组合式继承就是借用构造函数+引用一个父类型原型的浅拷贝对象

这种方法的高效体现在它只调用了一次Parent构造函数,并且因此避免了在Child.prototype上创建不必要的、多余的属性。与此同时原型链还能保持不变,因此还能正常使用instanceof和isPrototypeOf来判断对象类型。目前来看这种继承模式是引用类型最理想的继承范式了。

prototype和__proto__的关系及区别

想要理解prototype和__proto__的关系及区别,就得想了解他们分别属于谁又指向谁

prototype属于函数,它指向函数的原型对象。

__proto__属于对象,它指向构造这个对象的构造函数的原型。

举例说明

1
2
function Foo() {};
let foo = new Foo();

Foo拥有一个原型对象,Foo.prototype指向这个对象。而foo对象由Foo()函数构造而成,foo拥有__proto__属性,它指向构造foo对象的构造函数(也就是Foo)的原型对象

那么就是foo.__proto__ -> Foo.prototype

我们用一张图来分析__proto__和prototype的关系

proto1

我们从f2和f1切入分析这张图片。在这张图的左边是对象,图的中间是函数,图的右边是原型。f2和f1都是Foo类型的对象,那么它们的__proto__指向Foo.prototype。Foo.prototype的constructor指向Foo()函数,Foo()函数的prototype指向Foo.prototype。Foo.prototype本身也是一个对象,所以Foo.prototype的__proto__指向Object.prototype。Object.prototype.constructor指向Object()函数,Object虽然是个函数,但是它也拥有__proto__,指向Function.prototype。Function.prototype.constructor指向Function函数

new的原理

通过new创建对象经历四个步骤:

  • 创建一个新对象
  • 将那个构造函数的作用域赋给新对象
  • 执行构造函数中的代码
  • 返回新对象

我们通过JS代码来自己实现一个new

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let New = function(P) {
let o = {};
let arg = Array.prototype.slice.call(arguments, 1);
o.__proto__ = P.prototype;
P.prototype.constructor = P;
P.apply(o, arg);
return o;
}
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
}
let p1 = New(Person, 'li', 24, 'student');
文章目录
  1. 1. bind、call、apply的区别
  2. 2. 继承的实现及其比较
    1. 2.1. 原型链继承
    2. 2.2. 借用构造函数继承
    3. 2.3. 组合继承
    4. 2.4. 原型式继承
    5. 2.5. 寄生式继承
    6. 2.6. 寄生组合式继承
    7. 2.7. prototype和__proto__的关系及区别
  3. 3. new的原理
|