前端进阶之原型、原型链

1 篇文章 0 订阅
1 篇文章 0 订阅

目录

new本质

理解this

介绍下原型、原型链

理解构造函数下的prototype属性和实例化对象的__proto__属性的联系

介绍下继承

继承的方式

call、apply、bind的区别

Class与原型的关系

Object.create/Object assign原理


new本质

new做了什么:new运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

  1. 创建一个全新的对象。
  2. 将该对象内置的原型对象设置为构造函数prototype引用的那个原型对象。
    // 链接到原型,将 新对象fn的 _proto_ 指向 TEST 的 prototype
    function TEST(){
        this.a=1
    }
    var fn = new TEST();
    console.log(fn,fn.__proto__,TEST.prototype)

     

  3. 执行函数,函数的this会绑定在新创建的对象上。
  4. 如果函数没有返回其他对象(包括数组、函数、日期对象等),那么会自动返回这个新对象。
  5. 返回的那个对象为构造函数的实例。

new 运算符改变了函数的执行上下文,同时改变了return 语句的行为。实际上,使用new和构造函数很类似于传统的实现了类的语言:

function Fn(name,age,job){
  this.name = name;
  this.age = age;
  this.job = job;
}
Fn()

以上代码中这个函数只会返回undefined,并且执行上下文是window(全局)对象,无意间创建了3个全局变量name,age,job;

function Fn(name,age,job){
  this.name = name;
  this.age = age;
  this.job = job;
}
const newFN =  new Fn();
console.log(newFN);

当使用new 关键字来调用构造函数时,执行上下文从全局对象(window)变成一个空的上下文,这个上下文代表了新生成的实例。因此,this 关键字指向当前创建的实例。

默认情况下,如果你的构造函数中没有返回任何内容,就会返回this——当前的上下文。

要不然就返回任意非原始类型的值.

 

理解this

全局上下文:在全局运行上下文中(在任何函数体外部),this 指代全局对象window。

函数上下文:在函数内部,this的值取决于函数是如何调用的。

深入的解释就是:

  • 如果函数对象在被调用时是在作用域链上查找到的,则该函数中this的值为全局对象;
  • 如果函数对象在被调用时是在原型链上查找到的,则该函数中this的值为调用函数的对象;

例如:

作为对象方法:如以下代码函数以对象里的方法的方式进行调用时,它们的this由调用该函数的对象进行设置,this打印的结果本test本身。

var test = {
  age: 12,
  fn: function() {
    console.log(this);
    return this.age;
  }
};
test.fn(); // 12;

作为构造函数:

function Text(){
    this.age = 12
    console.log(this)
};
const fn = new Text();
console.log(fn)

 

介绍下原型、原型链

我们创建的每一个构造函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

理解原型:

  1. 我们只要创建了一个新的构造函数,就会根据一组特定的规则为该函数创建一个prototype属性,指向函数的原型对象。即Person(构造函数)有一个prototype指针,指向Person.prototype(Person.prototype是一个对象)。
  2. 默认情况下,每个原型对象上都会创建一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针
  3. 下面的图,可以更清晰的弄明白两者之间的关系。

构造函数和实例原型的关系图

 通过构造函数创建对象的过程我们称之为实例化,创建出来的对象称为实例;通过prototype创建出来的对象成为原型。

 

function Person(){
    this.age = 12;
};
Person.prototype.name='xiaoming';
var fn1 = new Person();
var fn2 = new Person();
console.log(fn2.prototype===fn1.prototype); // true

上述代码中函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型,也就是这个例子中的 fn1 和 fn2 的原型。

原型链:

当访问一个对象的属性或方法时,如果找不到,就会查找与这个对象关联的原型中的属性,如果还找不到,就再去找原型的原型,一直找到最顶层(Object)为止,而由原型组成的这个链条就叫做原型链。

__proto__是对象实例和它的构造函数之间建立的链接,它的值是:构造函数的prototype。

也就是说:__proto__的值是它所对应的原型对象,是某个函数的prototype。

注意:如果一个对象存在另一个对象的原型链上,我们可以说:它们是继承关系。

function foo() {
}

foo.prototype.name = 'xiaoming'

var person = new foo();
person.name = 'xiaohua';

console.log(person.name); // xiaohua
delete person.name;
console.log(person.name); // xiaoming

在代码中第一次输出 person.name 时,由于实例 person 自己有一个 name 属性,因此直接输出,不再向上查找。

而接下来我们删除了 person 自己的 name 属性,再次输出时由于自己属性上查找不到,因此就会沿着原型链向上查找,然后在 person 的原型对象里找到了 name 属性,查找结束,输出 xiaoming

constructor:

默认情况下,每个原型对象上都会有一个 constructor 属性,指向关联的构造函数,即 Person.prototype.constructor === Person。

 

如图构造函数、prototype、constructor的关系:每一个构造函数上面都有一个prototype属性指向prototype原型对象,prototype原型对象上面默认有一个constructor属性,指向构造函数,所以在控制台打印一个函数的时候点开constructor是一个无限的过程。

微信截图_20200229194518.png

结论:

  1. 原型的顶端: Object.prototype, 任何一个默认的内置的函数的原型都继承自Object.prototype.
  2.  原型链 : Js的对象结构中出现的指向Object.prototype的一系列原型对象,我们称之为原型链.
  3. 属性搜索原则 : 在访问对象的属性和方法时, 会在当前对象中查找, 如果没有找到, 会一直沿着原型链上的原型对象向上查找, 直到找到Object.prototype为止
  4. 写入原则 : 如果给对象设置属性和方法, 都是在当前对象上设置.

 

理解构造函数下的prototype属性和实例化对象的__proto__属性的联系

简单的一句话:对象有属性__proto__,指向该对象的构造函数的原型对象,如下代码:

function Test(){
    this.age = 10
};
var test = new Test();
test.__proto__ === Test.prototype; // true
test.constructor === Test.prototype.constructor; // true
test.constructor === Test; // true
  1. __proto__和constructor属性是对象所独有的;
  2. __proto__是由一个对象指向一个对象,即指向它们的原型对象。
  3. constructor是从一个对象指向一个函数,即指向它们的构造函数。
  4. prototype属性是函数所独有的,它是从一个函数指向一个对象

看到这里应该能理解最上面new的本质了吧,new创建了一个全新的对象(test),并且将该对象的原型对象(test.__proto__)设置为构造函数prototype(Test.prototype)引用的那个原型对象,然后执行函数,函数的this会绑定在新创建的对象上。

构造函数,通过prototype来存储要共享的属性和方法,也可以设置prototype指向现存的对象来继承该对象。

对象的__proto__指向自己构造函数的prototypeobj.__proto__.__proto__...的原型链由此产生,包括我们的操作符instanceof正是通过探测obj.__proto__.__proto__... === Constructor.prototype来验证obj是否是Constructor的实例。

原型链图

构造函数以及js原生Object对象之间的原型关系

image

参考《详解js原型,构造函数以及class之间的原型关系》

 

介绍下继承

定义:继承是一个类从另一个类(或多个)获取方法和属性的过程。

继承的方式

通过call、apply、bind构造函数继承、原型链继承等。

原型链继承的缺点的缺点:

  • 父类使用this声明的属性被所有实例共享
  • 创建子类实例时,无法向父类构造函数传参,不够灵活

call、apply、bind的区别

调用call/apply/bind的必须是个函数,下面语法中fn为函数

语法:

fn.call(thisArg, param1, param2, ...);
fn.apply(thisArg, [param1,param2,...]);
fn.bind(thisArg, param1, param2, ...);

区别:

call与apply的唯一区别:apply第二个参数是数组而call第二个开始的参数都是传给fn的;
call/apply与bind的区别:

  • call/apply改变了函数的this上下文后马上执行该函数

  • bind则是返回改变了上下文后的函数,不执行该函数

  • call/apply 返回fun的执行结果

  • bind返回fun的拷贝,并指定了fun的this指向,保存了fun的参数。

 function fatherFn(...arr) {
  this.some = '父类的this属性';
  this.params = arr
  console.log(this) // sonFn(){}
}
fatherFn.prototype.fatherFnSome = '父类原型对象的属性或者方法';
function sonFn(fatherParams, ...sonParams) {
  fatherFn.call(this, ...fatherParams);
  this.obkoro1 = '子类的this属性';
  this.sonParams = sonParams;
  console.log(this) // sonFn(){}
}
sonFn.prototype.sonFnSome = '子类原型对象的属性或者方法'
let fatherParamsArr = ['父类的参数1', '父类的参数2']
let sonParamsArr = ['子类的参数1', '子类的参数2']
const sonFnInstance = new sonFn(fatherParamsArr, ...sonParamsArr);
  1. 在子类中使用call调用父类,fatherFn将会被立即执行,并且将fatherFn函数的this指向sonFn的this。
  2. 因为函数执行了,所以fatherFn使用this声明的函数都会被声明到sonFn的this对象下。
  3. 实例化子类,this将指向new期间创建的新对象,返回该新对象。
  4. 对fatherFn.prototype没有任何操作,无法继承。

注意:返回其他对象会导致获取不到构造函数的实例,很容易因此引起意外的问题!

function Fn() {
  this.some = '父类的this属性';
  console.log(this); 
  return [1, 2, 3,]
}
var fs = new Fn();

 

Class与原型的关系

class类的定义

ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

class Point {
  constructor(){
    // ...
  }

  toString(){
    // ...
  }

  toValue(){
    // ...
  }
}

// 等同于

Point.prototype = {
  toString(){},
  toValue(){}
};

上述代码中,构造函数的prototype属性,在ES6的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。

ES6的类,完全可以看作构造函数的另一种写法。

class Bar {
  doStuff() {
    console.log('stuff');
  }
}

typeof Bar // "function"
Bar === Bar.prototype.constructor // true

var b = new Bar();
b.doStuff() // "stuff"

上面代码表明,类的数据类型就是函数,类本身就指向构造函数。

使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。

注意:ES5与ES6 实现继承中,在ES5中,继承实质上是子类先创建属于自己的this,然后再将父类的方法添加到this(也就是使用Parent.apply(this)的方式而在ES6中,则是先创建父类的实例对象this,然后再用子类的构造函数修改this。

如图:es5中的继承中通过call/apply把父类和子类的this都指向子类,子类的实例的__proto__指向子类的构造函数,然后顺着__proto__指向Function。这里面继承主要做的只是把父类的方法添加到this上。缺点:无法继承父类的原型上定义的方法。

再看看es6:这里实际上是通过把子类的constructor指向了父类。子类的实例的__proto__指向子类原型,然后顺着__proto__指向父类,再顺着__proto__指向Function。

ES6Class详细知识点看这里

 

Object.create/Object assign原理

 

 

 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值