原型(Prototype)
每个函数在创建时,都会自动获得一个 prototype
属性,这个属性指向一个对象,称为该函数的原型对象。当使用 new
操作符基于这个函数创建对象实例时,实例的内部 [[Prototype]]
(在大多数现代浏览器中,可以通过 __proto__
访问,但不推荐直接使用)会指向该函数的原型对象。
原型对象的主要用途是实现属性和方法的共享与复用。在原型对象上定义的属性和方法,可以被通过该构造函数创建的所有实例访问和使用。
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};
let person1 = new Person('Alice');
let person2 = new Person('Bob');
person1.sayHello();
person2.sayHello();
在上述示例中,sayHello
方法定义在 Person
函数的原型对象上,但 person1
和 person2
实例都可以调用它。
原型链(Prototype Chain)
当访问一个对象的属性或方法时,如果在对象本身找不到,JavaScript 引擎会沿着对象的 __proto__
所指向的原型对象继续查找。如果在当前原型对象中仍然找不到,就会继续沿着当前原型对象的 __proto__
向上查找,如此形成一个链式结构,这就是原型链。
原型链的顶端是 Object.prototype
,如果在整个原型链中都没有找到要访问的属性或方法,最终会返回 undefined
。
function Animal() {}
Animal.prototype.eat = function() {
console.log('Eating...');
};
function Dog() {}
Dog.prototype = new Animal();
let dog = new Dog();
dog.eat();
在这个例子中,创建的 dog
对象本身没有 eat
方法,但通过原型链在 Animal
的原型对象上找到了 eat
方法。
prototype与proto
prototype
:这是函数对象所特有的属性。当使用new
操作符基于一个函数创建对象实例时,新创建的对象的__proto__
会指向该函数的prototype
对象。例如,上面的构造函数Person
,Person.prototype
上定义的属性和方法可以被通过new Person()
创建的实例访问和继承。__proto__
:这是大多数对象都具有的一个内部属性(在 ES6 中,更推荐使用Object.getPrototypeOf()
和Object.setPrototypeOf()
方法来操作对象的原型)。它用于建立对象与原型对象之间的链接,从而实现属性的查找和继承。
简单来说,prototype
用于定义构造函数的原型,而 __proto__
用于连接对象和其对应的原型。
继承
说到原型就不得不说一下js中的继承。
继承的主要目的是代码复用和实现对象之间的层次关系。通过继承,可以创建具有相似特征和行为的对象类,减少重复代码,并使代码结构更清晰、更易于维护。
js中继承分为两种:基于原型的继承、基于class的继承
基于原型的继承方式
原型链继承
原理:将子类的原型对象设置为父类的实例
不足:
- 引用类型属性被共享
- 无法向父类构造函数传参。因为父类构造函数被挂载在prototype上, 所以子类new时传递进来的参数无法直接传递到Parent上
function Parent() {
this.colors = ["red", "blue"];
}
function Child() {}
Child.prototype = new Parent();
let child1 = new Child();
child1.colors.push("green");
let child2 = new Child();
console.log(child2.colors); // ["red", "blue", "green"] 引用类型被共享
借用构造函数继承
原理:在子类构造函数中通过 call()
或 apply()
方法调用父类构造函数来继承属性,解决了引用类型共享和传参问题.
不足:函数无法复用。child1、child2中都单独有一个独立的sayHello方法
function Parent(name) {
this.name = name;
this.colors = ["red", "blue"];
this.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};
}
function Child(name) {
Parent.call(this, name);
}
let child1 = new Child("John");
child1.colors.push("green");
let child2 = new Child(“bob”);
组合式继承
原理:结合原型链继承和借用构造函数继承,用原型链继承方法,用借用构造函数继承实例属性
不足:父类构造函数会被调用两次
function Parent(name) {
this.name = name;
this.colors = ["red", "blue"];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
let child = new Child("John", 10);
原型式继承
原理:创建一个函数, 在函数内部创建一个临时的构造函数,将函数传进来的对象作为构造函数的原型,然后返回这个临时构造的对象实例。
不足:引用类型属性被共享
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
let person = {name: "John"};
let anotherPerson = object(person);
寄生式继承
原理:基于原型式继承创建一个新对象,然后为新对象添加额外的属性和方法。
function createAnother(original) {
let clone = object(original);
clone.sayHi = function() {
console.log("Hi");
};
return clone;
}
let person = {name: "John"};
let anotherPerson = createAnother(person);
寄生组合式继承
原理:被认为是 JavaScript 中最理想的继承方式。它通过创建一个空的中间对象,将中间对象的原型指向父类的原型,然后将子类的原型指向这个中间对象,避免了父类构造函数的重复调用。
function inheritPrototype(subType, superType) {
let prototype = Object.create(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
function Parent() {}
Parent.prototype.someMethod = function() {}
function Child() {}
inheritPrototype(Child, Parent);
基于class的继承方式
extends-推荐实际使用
- 语法更清晰和简洁,更接近传统面向对象编程语言的语法,易于理解和使用。
extends
关键字明确表示了继承关系。- 通过
super
关键字方便地调用父类的构造函数和方法。 - 支持方法的重写和扩展,使得代码的结构更加清晰和可维护。
// 父类
class Parent {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(`Hello, I'm ${this.name}`);
}
}
// 子类继承父类
class Child extends Parent {
constructor(name, age) {
// 调用父类的构造函数
super(name);
this.age = age;
}
// 子类可以重写父类的方法
sayHello() {
console.log(`Hello, I'm ${this.name} and I'm ${this.age} years old`);
}
}
let child = new Child('John', 10);
child.sayHello();
call、apply、bind的实现
上面我们了解了原型、原型链以及继承的一些知识点。知道通过call、apply改变指针也能实现继承。那call、apply、bind是如果改变指针指向的呢?这个又跟原型、原型链有什么关系呢?
我们自己来实现一下这三个方法就明白了
call
Function.prototype.myCall = function(context, ...args){
// 判断调用对象是否为函数
if(typeof this !== 'function'){
throw new TypeError('not a function')
}
// 判断context是否传入,如果未传入则设置为window
context = context || window
// 给context创建一个Symbol属性以免覆盖原有的属性
const fn = Symbol()
context[fn] = this
// 调用函数
let res = context[fn](...args)
// 将属性删除
delete context[fn]
// 返回结果
return res
}
apply
apply与call的区别在于参数格式不同
Function.prototype.myApply = function (context, args) {
if(typeof this !== 'function'){
throw new TypeError('not a function');
}
context = context || window;
const fn = Symbol()
context[fn] = this
let res = context[fn](args);
delete context[fn];
return res;
}
bind
bind方法执行后会返回一个函数,结合apply可以改变返回的函数的指针指向
Function.prototype.myBind = function(context,...args){
let self = this;
console.log(args);
return function(){
return self.myApply(context,args)
}
}