跳到主要内容

原型链

原型是通过__proto__指向的,把原型连接成链就叫原型链,原型链记录了原型创建对象的整个过程,原型链是原型创建对象的历史记录。

JS如何实现继承?

第一种: 借助call

  function Parent1(){
this.name = 'parent1';
}
function Child1(){
Parent1.call(this);
this.type = 'child1'
}
console.log(new Child1);

这样写的时候子类虽然能够拿到父类的属性值,但是问题是父类原型对象中一旦存在方法那么子类无法继承。那么引出下面的方法。

第二种: 借助原型链

  function Parent2() {
this.name = 'parent2';
this.play = [1, 2, 3]
}
function Child2() {
this.type = 'child2';
}
Child2.prototype = new Parent2();

console.log(new Child2());

看似没有问题,父类的方法和属性都能够访问,但实际上有一个潜在的不足。举个例子:

  var s1 = new Child2();
var s2 = new Child2();
s1.play.push(4);
console.log(s1.play, s2.play);

第三种:将前两种组合

  function Parent3 () {
this.name = 'parent3';
this.play = [1, 2, 3];
}
function Child3() {
Parent3.call(this);
this.type = 'child3';
}
Child3.prototype = new Parent3();
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play);

之前的问题都得以解决。但是这里又徒增了一个新问题,那就是Parent3的构造函数会多执行了一次(Child3.prototype = new Parent3();)。这是我们不愿看到的。那么如何解决这个问题?

第四种: 组合继承的优化1

  function Parent4 () {
this.name = 'parent4';
this.play = [1, 2, 3];
}
function Child4() {
Parent4.call(this);
this.type = 'child4';
}
Child4.prototype = Parent4.prototype;

这里让将父类原型对象直接给到子类,父类构造函数只执行一次,而且父类属性和方法均能访问,但是我们来测试一下:

  var s3 = new Child4();
var s4 = new Child4();
console.log(s3)

子类实例的构造函数是Parent4,显然这是不对的,应该是Child4。

第五种(最推荐使用): 组合继承的优化1

  function Parent5 () {
this.name = 'parent5';
this.play = [1, 2, 3];
}
function Child5() {
Parent5.call(this);
this.type = 'child5';
}
Child5.prototype = Object.create(Parent5.prototype);
Child5.prototype.constructor = Child5;

这是最推荐的一种方式,接近完美的继承,它的名字也叫做寄生组合继承。

ES6的extends被编译后的JavaScript代码 TODO

实现new

要创建一个实例,应该使用new操作符,用new来调用构造函数会执行如下操作

  1. 创建一个新对象;
  2. 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性,实现继承
  3. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
  4. 执行构造函数中的代码(为这个新对象添加属性);
  5. 返回新对象,如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
function _new(fn, ...arg) {
const obj = {}; //创建一个新的对象
obj.__proto__ = fn.prototype; //把obj的__proto__指向fn的prototype,实现继承
fn.apply(obj, arg) //改变this的指向
return Object.prototype.toString.call(obj) == '[object Object]'? obj : {} //返回新的对象obj
}

所以,这也是为什么new可以改变this指向的原因,回答了刚刚开始的时候那个问题

Object.create 和 new 有什么区别?

js中创建对象的方式一般有两种Object.create和new

1const Base = function(){};
2const o1 = Object.create(Base);
3const o2 = new Base();

在讲述两者区别之前,我们需要知道:

  • 构造函数Foo的原型属性Foo.prototype指向了原型对象。
  • 原型对象保存着实例共享的方法,有一个指针constructor指回构造函数。
  • js中只有函数有 prototype 属性,所有的对象只有 proto 隐式属性。

那这样到底有什么不一样呢?

Object.create

先来看看 Object.create 的实现方式

1Object.create =  function (o) {
2 var F = function () {};
3 F.prototype = o;
4 return new F();
5};

可以看出来。Object.create是内部定义一个对象,并且让F.prototype对象 赋值为引进的对象/函数 o,并return出一个新的对象。

new

再看看 const o2 = new Base() 的时候,new做了什么。

1var o1 = new Object();
2o1.[[Prototype]] = Base.prototype;
3Base.call(o1);

new做法是新建一个obj对象o1,并且让o1的__proto__指向了Base.prototype对象。并且使用 call 进行强转作用环境。从而实现了实例的创建。

区别

看似是一样的。我们对原来的代码进行改进一下。

1var Base = function () {
2 this.a = 2
3}
4var o1 = new Base();
5var o2 = Object.create(Base);
6console.log(o1.a); // 2
7console.log(o2.a); // undefined

可以看到Object.create 失去了原来对象的属性的访问。

再进行下改造:

1var Base = function () {
2 this.a = 2
3}
4Base.prototype.a = 3;
5var o1 = new Base();
6var o2 = Object.create(Base);
7console.log(o1.a); // 2
8console.log(o2.a); // undefined

小结

比较newObject.create
构造函数保留原构造函数属性丢失原构造函数属性
原型链原构造函数prototype属性原构造函数/(对象)本身
作用对象functionfunction和object

Function.proto(getPrototypeOf)是什么?

获取一个对象的原型,在 chrome 中可以通过_proto_的形式,或者在 ES6 中可以通过Object.getPrototypeOf 的形式。 那么 Function.proto 是什么么?也就是说 Function 由什么对象继承而来,我们来做如下判别。 Function.proto==Object.prototype //false Function.proto==Function.prototype//true 我们发现 Function 的原型也是 Function。

对原型、原型链的理解

原型和原型链是 JavaScript 中的重要概念,用于实现对象的继承和属性查找。

  1. 原型(Prototype):

    • 每个 JavaScript 对象都有一个原型(prototype),它是一个对象,可以包含共享的属性和方法。
    • 对象可以通过 __proto__ 属性访问它的原型。
    • 当访问对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或方法或到达原型链的末尾(即 null)。
  2. 原型链(Prototype Chain):

    • 原型链是一系列对象的链接,每个对象都有一个指向其原型的引用。
    • 当访问对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或方法或到达原型链的末尾(即 null)。
    • 每个对象的原型又是另一个对象,通过这种方式形成了一个层级关系,即原型链。
    • 原型链的终点是 Object.prototype,它是大多数对象的最顶层原型,包含一些常用的方法和属性(如 toString()valueOf() 等)。

通过原型和原型链,JavaScript 实现了对象的继承和属性的共享:

  • 当访问对象的属性或方法时,会先在对象本身查找,如果找不到,会继续在其原型上查找,然后在原型的原型上查找,以此类推,直到找到或到达原型链的末尾。
  • 如果对象的原型上也没有该属性或方法,那么会继续在原型的原型上查找,直到找到或到达原型链的末尾。
  • 这样就可以实现属性和方法的共享,避免在每个对象上都创建一份相同的属性和方法。

示例代码如下:

// 创建一个对象 person
const person = {
name: 'John',
age: 30,
};

// person 对象的原型是 Object.prototype
console.log(person.__proto__ === Object.prototype); // true

// 在 person 对象上访问 toString 方法,由于 person 对象没有该方法,会在原型链上查找并找到 Object.prototype 上的 toString 方法
console.log(person.toString()); // "[object Object]"

// 创建一个对象 student
const student = {
school: 'XYZ School',
};

// student 对象的原型设置为 person 对象
Object.setPrototypeOf(student, person);

// student 对象的原型是 person 对象
console.log(student.__proto__ === person); // true

// 在 student 对象上访问 name 属性,由于 student 对象没有该属性,会在原型链上查找并找到 person 对象上的 name 属性
console.log(student.name); // "John"

//

原型链的终点是什么?如何打印出原型链的终点?

原型链的终点是 Object.prototypeObject.prototype 是 JavaScript 中大多数对象的最顶层原型,它是原型链的末尾,没有自己的原型。

可以通过以下方法打印出原型链的终点:

// 创建一个对象
const obj = {};

// 打印原型链的终点
let prototype = Object.getPrototypeOf(obj);
while (prototype !== null) {
console.log(prototype);
prototype = Object.getPrototypeOf(prototype);
}

在上述代码中,我们使用 Object.getPrototypeOf() 方法来获取对象的原型。通过一个循环,我们依次打印出每个原型对象,直到原型为 null,即到达了原型链的终点。注意,打印结果中会显示一些内置的原型对象,如 Object.prototypeArray.prototypeFunction.prototype 等。

请注意,在控制台中打印整个原型链可能会导致很长的输出,特别是对于复杂对象或原型链较深的对象。因此,建议在实际开发中根据需求选择打印原型链的某个部分或特定对象的原型链。

对作用域、作用域链的理解

作用域和作用域链是 JavaScript 中关于变量访问和可见性的重要概念。

  1. 作用域(Scope):

    • 作用域是一个定义了变量和函数可访问性的规则集。
    • JavaScript 中有全局作用域和局部作用域。
    • 全局作用域是整个 JavaScript 程序环境中可访问的范围。
    • 局部作用域是在函数内部定义的范围,其中的变量和函数只能在该函数内部访问。
  2. 作用域链(Scope Chain):

    • 作用域链是一系列嵌套的作用域的集合,用于确定变量和函数的访问规则。
    • 当访问一个变量或函数时,JavaScript 引擎会先在当前作用域中查找,如果找不到,则会沿着作用域链向上一级一级地查找,直到找到该变量或函数或到达全局作用域。
    • 作用域链的顶端是当前作用域,底端是全局作用域。
  3. 作用域和作用域链的影响:

    • 变量和函数在作用域内是可见的,可以被访问和使用。
    • 在一个作用域中定义的变量和函数可以被该作用域及其子作用域访问。
    • 子作用域可以访问父作用域中的变量和函数,但父作用域无法访问子作用域中的变量和函数。
    • 如果在当前作用域和父作用域中都找不到变量或函数,就会抛出一个 ReferenceError。

示例代码如下:

// 全局作用域
const globalVariable = 'Global'; // 全局变量

function outer() {
// 外部函数作用域
const outerVariable = 'Outer'; // 外部函数作用域变量

function inner() {
// 内部函数作用域
const innerVariable = 'Inner'; // 内部函数作用域变量

console.log(innerVariable); // 可以访问内部作用域变量
console.log(outerVariable); // 可以访问外部作用域变量
console.log(globalVariable); // 可以访问全局作用域变量
}

inner(); // 调用内部函数
}

outer(); // 调用外部函数

在上述代码中,全局作用域包含 globalVariable 变量和 outer 函数。outer 函数作用域包含 outerVariable 变量和 inner 函数。inner 函数作用域包含 innerVariable 变量。变量可以在其定义的作用域及其父级作用域

中访问,但无法在子作用域中访问。通过作用域链的方式,内部函数可以访问外部函数和全局作用域中的变量。