JavaScript中的原型与原型链

简介: JavaScript中的原型与原型链

js中的原型与原型链

原型基础

原型对象

每个对象都有一个原型prototype对象,通过函数创建的对象也将拥有这个原型对象。原型是一个指向对象的指针。

  • 可以将原型理解为对象的父亲,对象从原型对象继承来属性
  • 原型就是对象除了是某个对象的父母外没有什么特别之处
  • 所有函数的原型默认是 Object的实例,所以可以使用toString/toValues/isPrototypeOf 等方法的原因
  • 使用原型对象为多个对象共享属性或方法
  • 如果对象本身不存在属性或方法将到原型上查找
  • 使用原型可以解决,通过构建函数创建对象时复制多个函数造成的内存占用问题
  • 原型包含 constructor 属性,指向构造函数
  • 对象包含 __proto__ 指向他的原型对象

其中:__proto__(谷歌浏览器已更新为[[prototype]],隐式原型)

我们可以用Object.getPrototypeOf来看他的原型

let x = [];
let y = {};
console.log(Object.getPrototypeOf(x)); //Array(0)
console.log(Object.getPrototypeOf(y)); //Object

下面两个变量的原型都是对象

let x = {};
let y = {};
console.log(Object.getPrototypeOf(x) === Object.getPrototypeOf(y)); //true

我们也可以创建一个极简对象(完全数据字典对象)没有原型(原型为 null)

let obj = {name:'kevin'};
console.log(obj.hasOwnProperty('name'));
let user = Object.create(null, {
name: {value: 'xj'}
});
// console.log(user.hasOwnProperty('name'));//Error

//Object.keys是静态方法,不是原型方法所以是可以使用的
console.log(Object.keys(user));

函数对象拥有多个原型

函数拥有两个原型,prototype 用于实例对象使用(用他这个构造函数new出来的对象),__proto__用于函数对象使用(他自己)

系统构造函数的原型体现

声明的变量的类型,他的原型__proto__指向创建其类型的构造函数(也可以叫类)的prototype属性

prototype属性的作用是为由这个构造函数创建的对象提供统一的方法

let arr = [];
console.log(arr.__proto__ === Array.prototype);//true

let obj = {};
console.log(obj.__proto__ === Object.prototype);//true

let str = '';
console.log(str.__proto__ === String.prototype);//true

原型链

调用对象上没有的方法,会在他的原型对象上找,找不到就在往上,以此类推。如果在Object构造函数的prototype中没找到,就返回null

构造函数的继承

new User()是对User构造函数的继承,那么我们只需要让Admin.prototype属性等于new User()就能让Admin继承User的方法和属性

function User(name, age){
  this.name = name;
  this.age = age;
  this.show =()=>{
    console.log(this.name,this.age);
  }
}

function Admin(){}
Admin.prototype = new User('kevin',12);
let admin = new Admin();
admin.show();

new命令的原理

使用new命令时,它后面的函数调用就不是正常的调用,而是依次执行下面的步骤。

  1. 创建一个空对象,作为将要返回的对象实例
  2. 将这个空对象的原型,指向构造函数的prototype属性
  3. 将这个空对象赋值给函数内部的this关键字
  4. 开始执行构造函数内部的代码

也就是说,构造函数内部,this指的是一个新生成的空对象,所有针对this的操作,都会发生在这个空对象上。构造函数之所以叫“构造函数”,就是说这个函数的目的,就是操作一个空对象(即this对象),将其“构造”为需要的样子。

如果构造函数内部有return语句,而且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象。

构造函数

原型中的constructor

function User(){};
console.log(User.prototype.constructor === User);//true

如果更改User.prototype的指向,记得在对象中加上构造函数的属性,即constructor: User,不然无法使用实例对象的constructor属性来构造新的实例对象。

我们也可以通过构造的实例对象找到构造函数,并且再次构造实例对象

function User(name,age){
  this.name = name;
  this.age = age;
  this.show = ()=>{
    return `${name}-${age}`;
  }
};
let user = new User('kevin', 20);
function creatByObj(obj, ...args){
  //声明一个常量等于该对象的原型的constructor(构造函数)
  const constructor = Object.getPrototypeOf(obj).constructor;
  return new constructor(...args);//返回一个通过constructor构造的实例对象
}
let admin = creatByObj(user, 'tom', 21);
console.log(admin.show());//tom-21

原型检测

instanceof 检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

function A() {}
function B() {}
function C() {}

const c = new C();
B.prototype = c;
const b = new B();
A.prototype = b;
const a = new A();

console.dir(a instanceof A); //true
console.dir(a instanceof B); //true
console.dir(a instanceof C); //true
console.dir(b instanceof C); //true
console.dir(c instanceof B); //false

使用isPrototypeOf检测一个对象是否是另一个对象的原型链中

function A() {}
function B() {}
function C() {}

const c = new C();
B.prototype = c;
const b = new B();
A.prototype = b;
const a = new A();
console.log(C.prototype.isPrototypeOf(b));//true

属性遍历

使用in 检测原型链上是否存在属性,使用 hasOwnProperty 只检测当前对象

let a = {url: 'dd'};
let b = {name: 'kevin'};
Object.setPrototypeOf(a,b);
console.log('name' in a);//true
console.log(a.hasOwnProperty('name'));//false

我们在使用for/in的时候,经常会看到这样的语句

let a = {url: 'dd'};
let b = {name: 'kevin'};
Object.setPrototypeOf(a,b);

for (const key in a) {
  if (Object.hasOwnProperty.call(a, key)) {
    const element = a[key];
    console.log(element);//只输出dd (a.url)
  }
}

如果没有该if语句,则会遍历a的原型链上的属性

let a = {url: 'dd'};
let b = {name: 'kevin'};
let c = {age: 22}

Object.setPrototypeOf(b,c)
Object.setPrototypeOf(a,b);

for (const key in a) {
    console.log(a[key]);//换行输出dd(a.url)  kevin(b.name)  22(c.age)
}

借用原型

使用 callapply 可以借用其他原型方法完成功能。

下面的lesson没有max方法,但是可以通过arr.max.apply(或者call)来更改this的指向,使this指向lesson

let arr = {data: [1,2,3,4,5,6,7]};
Object.setPrototypeOf(arr,{
  max(){
    return this.data.sort((a, b) => b - a)[0];
  }
});
console.log(arr.max());//7
let lesson = {
  thsLesson: {node: 77, ts: 66, js: 88},
  get data(){
    return Object.values(this.thsLesson);
  }
};
console.log(arr.max.apply(lesson));//88

如果max方法需要传参,则可以这样写

let arr = {data: [1,2,3,4,5,6,7]};
Object.setPrototypeOf(arr,{
  max(data){
    return data.sort((a, b) => b - a)[0];
  }
});
console.log(arr.max(arr.data));//7
let lesson = {
  thsLesson: {node: 77, ts: 66, js: 88},
};
//因为只用传一个参数,所以用call。如果使用apply就变成了传递多个参数,因为Object.values返回值是个数组。在这里我们只需要传一个参数(并且是一个数组)
console.log(arr.max.call(null, Object.values(lesson.thsLesson)));//88

当然,我们也可以使用Math.max()方法

let arr = {data: [1,2,3,4,5,6,7]};
console.log(Math.max(...arr.data));//7
let lesson = {
  thsLesson: {node: 77, ts: 66, js: 88},
};
console.log(Math.max(...Object.values(lesson.thsLesson)));//88

下面是获取设置了 class 属性的按钮,但 DOM 节点不能直接使用数组的filter 等方法,但借用数组的原型方法就可以操作了。

<body>
  <button>1</button>
  <button class="dd">2</button>
</body>
<script>
  let btns = document.querySelectorAll('button');
  console.log('filter' in btns);//btns原型链上没有filter方法
  console.log('filter' in []);//数组的原型链上有filter方法
  //所以我们可以通过call或者apply来借用方法
  btns = [].filter.call(btns, item => item.hasAttribute('class'));
  console.log(btns);//[button.dd]
</script>

使用优化

使用构造函数会产生函数复制造成内存占用,我们可以将方法写入构造函数的prototype属性中。

function User(name){
  this.name = name;
}
User.prototype = {
  //记得加上constructor属性,不然不能通过实例对象的constructor属性构建新的实例对象
  constructor: User,
  show(){
    console.log(this.name);
  },
  get(){
    console.log('get...');
  }
};
let zs = new User('张三');
zs.show();
let ls = new User('李四');
ls.show();

原型总结

this与原型没啥关系

this与原型没啥关系,因为this指向的是调用该属性的对象,或者是call/apply/bind新的指向

不要滥用原型

如果引用了第三方库,可能属性会同名,后加载的会覆盖之前的。所以要慎用原型。

Object.create和\_proto_

使用Object.create创建一个新对象时使用现有对象做为新对象的原型对象

第二个参数为设置新对象的属性(一个对象)

__proto__ 属性只有浏览器必须部署,在非浏览器的环境不一定部署。

let user = {
  show(){
    return this.name;
  }
};
let admin = Object.create(user, {
  name: {
    value: 'adminadmin'
  }
});
// let admin = {name: 'adminadmin'};
// admin.__proto__ = user;

console.log(admin.show());//adminadmin
console.log(admin);//{name: 'adminadmin'}
console.log(admin.__proto__);//{show: ƒ}

Object.setPrototypeOf

关于原型的设置,也可以使用Object.setPrototypeOf方法

let user = {
  show(){
    return this.name;
  }
};

Object.setPrototypeOf(admin, user);

console.log(admin.show());//adminadmin
console.log(admin);//{name: 'adminadmin'}
console.log(admin.__proto__);//{show: ƒ}

\__proto__是一个属性访问器

__proto__实际上是一个属性访问器,只允许对象或 null设置\__proto__属性。

如果想要让obj的\__proto__属性等于一个字符串或者数值,可以改变obj的原型。

let obj = {};
obj.__proto__ = 'kevin';
console.log(obj.__proto__ === 'kevin');//false
Object.setPrototypeOf(obj, null);
obj.__proto__ = 'kevin';
console.log(obj.__proto__ === 'kevin');//true

继承与多态

当对象中没使用的属性时,JS 会从原型上获取这就是继承在 JavaScript 中的实现

体验继承

function A(){
  this.name = 'a';
}
function B(){
  this.age = 22
}
B.prototype = new A();
let b = new B();
console.log(b.name);// a

继承是原型的继承,而不是改变构造函数的原型

下面是把某构造函数的prototype属性添加到原型链上

function User(){
  this.name = 'kevin';
}
function Admin(){}
//在原型上增加方法,通过原型的继承(让构造函数的prototype属性的原型为另一个构造函数的prototype属性)
//在原型链上增加原型后,不会使role方法不能被调用
Admin.prototype.role = function(){
  console.log('a role');
}
Admin.prototype.__proto__ = User.prototype;
let admin = new Admin();
admin.role();// a role

也可以使用Object.crate在原型链上添加构造函数的prototype。

但是需要添加构造函数的属性,因为crate是创建了一个新的对象。

在添加完构造函数属性后,还需要设置该属性不能被遍历

方法重写

下而展示的是子类需要重写父类方法的技巧。

function Person() {}
Person.prototype.getName = function() {
  console.log("parent method");
};

function User(name) {}
User.prototype = Object.create(Person.prototype);
User.prototype.constructor = User;

User.prototype.getName = function() {
  //调用父级同名方法
  Person.prototype.getName.call(this);
  console.log("child method");
};
let hd = new User();
hd.getName();

多态

根据多种不同的形态产生不同的结果,下而会根据不同形态的对象得到了不同的结果。

function User() {}
User.prototype.show = function() {
  console.log(this.description());
};

function Admin() {}
Admin.prototype = Object.create(User.prototype);
Admin.prototype.description = function() {
  return "管理员在此";
};

function Member() {}
Member.prototype = Object.create(User.prototype);
Member.prototype.description = function() {
  return "我是会员";
};

function Enterprise() {}
Enterprise.prototype = Object.create(User.prototype);
Enterprise.prototype.description = function() {
  return "企业帐户";
};

for (const obj of [new Admin(), new Member(), new Enterprise()]) {
  obj.show();
}

深挖继承

继承是为了复用代码,继承的本质是将原型指向到另一个对象。

调用父类构造函数完成对象的属性初始化

我们希望调用父类构造函数完成对象的属性初始化。

下面是使用示例,我们可以使用call或者apply来使User的this指向Admin

function User(name,age){
  this.name = name;
  this.age = age;
}
User.prototype.show = function(){
  console.log(this.name,this.age);
};
function Admin(...args){
  User.apply(this,args);
}
Admin.prototype.__proto__ = User.prototype;
let admin = new Admin('kevin', 20);
admin.show();//kevin 20

原型工厂

原型工厂是将继承的过程封装,使用继承业务简单化。

function extend(sub, sup){
  sub.prototype = Object.create(sup.prototype);
  sub.prototype.constructor = sub;
}
function Access(){};
function User(){};
function Admin(){};
function Member(){};

extend(User, Access);//User继承Access
extend(Admin, User);//Admin继承User
extend(Member, Access)//Member继承Access

Access.prototype.role = function(){};
User.prototype.getItem = function(){};

console.log(new Admin());// 继承关系: Admin>User>Access>Object
console.log(new Member());//继承关系:Member>Access>Object

对象工厂

在原型继承基础上,将对象的生成使用函数完成,并在函数内部为对象添加属性或方法。

function User(name, age) {
  this.name = name;
  this.age = age;
}
User.prototype.show = function() {
  console.log(this.name, this.age);
};

function Admin(name, age) {
  let instance = Object.create(User.prototype);
  User.call(instance, name, age);
  instance.role=function(){
    console.log('admin.role');
  }
  return instance;
}
let hd = Admin("后盾人", 19);
hd.show();

function member(name, age) {
  let instance = Object.create(User.prototype);
  User.call(instance, name, age);
  return instance;
}
let lisi = member("李四", 28);
lisi.show();

Mixin 模式

JS不能实现多继承,如果要使用多个类的方法时可以使用mixin混合模式来完成。

  • mixin 类是一个包含许多供其它类使用的方法的类
  • mixin 类不用来继承做为其它类的父类

下面是示例中 Admin需要使用 Request.prototypeCredit 的功能,因为JS 是单继承,我们不得不将无关的类连接在一下,显然下面的代码实现并不佳

function extend(sub, sup) {
  sub.prototype = Object.create(sup.prototype);
  sub.prototype.constructor = sub;
}
function Credit() {}
function Request() {}
function User(name, age) {
  this.name = name;
  this.age = age;
}
extend(Request, Credit);
extend(User, Request);
Credit.prototype.total = function() {
  console.log("统计积分");
};
Request.prototype.ajax = function() {
  console.log("请求后台");
};
User.prototype.show = function() {
  console.log(this.name, this.age);
};
function Admin(...args) {
  User.apply(this, args);
}
extend(Admin, User);
let hd = new Admin("向军", 19);
hd.show();
hd.total(); //统计积分
hd.ajax(); //请求后台

下面分拆功能使用 Mixin 实现多继承,使用代码结构更清晰。只让 Admin 继承 User 原型

function extend(sub, sup) {
  sub.prototype = Object.create(sup.prototype);
  sub.prototype.constructor = sub;
}
function User(name, age) {
  this.name = name;
  this.age = age;
}
User.prototype.show = function() {
  console.log(this.name, this.age);
};
const Credit = {
  total() {
    console.log("统计积分");
  }
};
const Request = {
  ajax() {
    console.log("请求后台");
  }
};

function Admin(...args) {
  User.apply(this, args);
}
extend(Admin, User);
Object.assign(Admin.prototype, Request, Credit);
let hd = new Admin("向军", 19);
hd.show();
hd.total(); //统计积分
hd.ajax(); //请求后台

mixin 类也可以继承其他类,比如下面的 Create 类获取积分要请求后台,就需要继承 Request 来完成。

  • super 是在 mixin 类的原型中查找,而不是在 User 原型中
function extend(sub, sup) {
  sub.prototype = Object.create(sup.prototype);
  sub.prototype.constructor = sub;
}
function User(name, age) {
  this.name = name;
  this.age = age;
}
User.prototype.show = function() {
  console.log(this.name, this.age);
};
const Request = {
  ajax() {
    return "请求后台";
  }
};
const Credit = {
  __proto__: Request,
  total() {
    console.log(super.ajax() + ",统计积分");
  }
};

function Admin(...args) {
  User.apply(this, args);
}
extend(Admin, User);
Object.assign(Admin.prototype, Request, Credit);
let hd = new Admin("向军", 19);
hd.show();
hd.total(); //统计积分
hd.ajax(); //请求后台

mixin模式的核心就是将一些功能剥离出来,然后再压入构造函数的prototype属性这个对象中

相关文章
|
JavaScript 前端开发 Java
深入JS面向对象(原型-继承)(一)
深入JS面向对象(原型-继承)
31 0
|
1月前
|
JavaScript 前端开发
js开发:请解释原型继承和类继承的区别。
JavaScript中的原型继承和类继承用于共享对象属性和方法。原型继承利用原型链查找属性,节省内存但不支持私有成员。类继承通过ES6的class和extends实现,支持私有成员但占用更多内存。两者各有优势,适用于不同场景。
19 0
|
3月前
|
JavaScript 前端开发
深入理解JavaScript中的原型链
本文将深入探讨JavaScript中的原型链机制,从根本上理解它的工作原理以及在开发中的应用。我们将介绍原型链的概念、如何创建和使用原型、原型链的继承机制以及一些常见的原型链相关问题。通过对原型链的详细解析,读者将能够更好地理解JavaScript中的继承、原型对象和原型链之间的关系,提高代码的质量和可维护性。
|
3月前
|
存储 前端开发 JavaScript
揭秘原型链:探索 JavaScript 面向对象编程的核心(下)
揭秘原型链:探索 JavaScript 面向对象编程的核心(下)
揭秘原型链:探索 JavaScript 面向对象编程的核心(下)
|
1月前
|
JavaScript
JS数组增删方法的原理,使用原型定义
JS数组增删方法的原理,使用原型定义
|
8天前
|
JavaScript
什么是js的原型链
什么是js的原型链
|
1月前
|
JavaScript
JS原型对象prototype
JS原型对象prototype
|
1月前
|
JavaScript 前端开发
在JavaScript中,如何优化原型链的性能?
在JavaScript中,如何优化原型链的性能?
16 2
|
1月前
|
JavaScript 前端开发
谈谈对 JavaScript 中的原型链的理解。
谈谈对 JavaScript 中的原型链的理解。
18 1
|
1月前
|
JavaScript 前端开发
js继承的超详细讲解:原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承、class继承
js继承的超详细讲解:原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承、class继承
55 0