一.什么是原型?
Javascript中有一句话,叫一切皆是对象,当然这句话也不严谨,比如null
和undefined
就不是对象,除了这俩完全可以说Javascript一切皆是对象。而Javascript对象都有一个叫做原型的公共属性,属性名是 __proto__
。这个原型属性是对另一个对象的引用,通过这个原型属性我们就可以访问另一个对象所有的属性和方法。比如:
let numArray = [1, 2, -8, 3, -4, 7];
Array 实例化的对象就有一个原型属性指向Array.prototype,变量numArray
继承了Array.prototype对象所有的属性和方法。
这就是为什么可以直接调用像sort() 这种方法:
console.log(numArray.sort()); // -> [-4, -8, 1, 2, 3, 7]
也就是说:
numArray.__proto__ === Array.prototype // true
对于其他对象(函数)也是一样(比如Date(),Function(), String(),Number() 等);
当一个构造函数被创建后,实例对象会继承构造函数的原型属性,这是构造函数的一个非常重要的特性。在Javascript中使用new
关键字来对构造函数进行实例化。看下面的例子:
const Car = function(color, model, dateManufactured) {
this.color = color;
this.model = model;
this.dateManufactured = dateManufactured;
};
Car.prototype.getColor = function() {
return this.color;
};
Car.prototype.getModel = function() {
return this.model;
};
Car.prototype.carDate = function() {
return `This ${this.model} was manufactured in the year ${this.dateManufactured}`
}
let firstCar = new Car('red', 'Ferrari', '1985');
console.log(firstCar);
console.log(firstCar.carDate());
上面的例子中,方法getColor,carDate,getModel都是对象(函数)Car的方法,而Car的实例对象firstCar可以继承Car原型上的一切方法和属性。
结论:
- js分为函数对象和普通对象,每个对象都有__proto__属性,指向它的构造函数的原型对象,但是只有函数对象才有prototype属性。
- Object、Function都是js内置的函数, 类似的还有我们常用到的Array、RegExp、Date、Boolean、Number、String
那么__proto__和prototype到底是什么?
属性__proto__是一个对象,它有两个属性,constructor和__proto__,constructor属性,用于记录实例是由哪个构造函数创建;
二.什么是原型链?
在Javascript中如果访问一个对象本身不存在的属性或是方法,就首先在它的原型对象上去寻找,如果原型对象上也不存在,就继续在原型对象的原型对象上去寻找,直到找到为止。那么原型对象有尽头么?所有对象的原型尽头是Object.prototype,那么Object.prototype这个对象的 __proto__
指向啥呢?答案是null
。我们日常开发中用到的绝大多数对象的 __proto__
基本不会直接指向Object.prototype
,基本都是指向另一个对象。比如所有的函数的 __proto__
都会指向Function.prototype,所有数组的 __proto__
都会指向Array.prototype。
let protoRabbit = {
color: 'grey',
speak(line) {
console.log(`The ${this.type} rabbit says ${line}`);
}
};
let killerRabbit = Object.create(protoRabbit);
killerRabbit.type = "assassin";
killerRabbit.speak("SKREEEE!");
上面代码中变量protoRabbit设置为所有兔子对象的公有属性对象集,killerRabbit这只兔子通过Object.create方法继承了protoRabbit的所有属性和方法,然后给killerRabbit赋值了一个type属性,再看下面的代码:
let mainObject = {
bar: 2
};
// create an object linked to `anotherObject`
let myObject = Object.create( mainObject );
for (let k in myObject) {
console.log("found: " + k);
}
// found: bar
("bar" in myObject);
如上变量myObject本身并没有bar属性,但这里会循着原型链一层一层往上找,直到找到或者原型链结束为止。如果到原型链尽头还是没找到该属性,那么访问该属性的时候就会返回undefined
了。
使用for...in关键字对对象进行迭代的过程,和上面访问某个属性循着原型链查找类似,会去遍历所有原型链上的属性(不论属性是否可枚举)。
let protoRabbit = {
color: 'grey',
speak(line) {
console.log(`The ${this.type} rabbit says ${line}`);
}
};
let killerRabbit = Object.create(protoRabbit);
killerRabbit.type = "assassin";
killerRabbit.speak("SKREEEE!");
上面的代码中访问speak的使用率很高,但如果我们想创建很多个Rabbit对象,就必须要重复写很多代码。而这正是原型和构造函数的真正用武之地。
let protoRabbit = function(color, word, type) {
this.color = color;
this.word = word;
this.type = type;
};
protoRabbit.prototype.getColor = function() {
return this.color;
}
protoRabbit.prototype.speak = function() {
console.log(`The ${this.type} rabbit says ${this.word}`);
}
let killerRabbit = new protoRabbit('grey', 'SKREEEEE!', 'assassin');
killerRabbit.speak();
如上代码,使用构造函数的方式就可以节省很多的代码。
附赠一张原型链的图:
js之父在设计js原型、原型链的时候遵从以下两个准则
:
- Person.prototype.constructor == Person // 准则1:原型对象(即Person.prototype)的constructor指向构造函数本身
- person.proto == Person.prototype // 准则2:实例(即person)的__proto__和原型对象指向同一个地方
创建对象的六种方法
1.字面量对象
这是比较常用的一种方式:
let obj = {};
2.object方式创建
先通过object构造器new一个对象,再往里丰富成员信息。
var obj = new Object();
obj.name = "dongjc";
obj.age = 32;
obj.Introduce = function () {
alert("My name is " + this.name + ".I'm " + this.age);
};
obj.Introduce();
3.Object.create方式创建
ECMAScript 5 中引入了一个新方法: Object.create()
。可以调用这个方法来创建一个新对象。新对象的原型就是调用 create 方法时传入的第一个参数:
var a = {a: 1};
// a ---> Object.prototype ---> null
var b = Object.create(a);
b.__proto__ === a; // true
优点: 支持当前所有非微软版本或者 IE9 以上版本的浏览器。允许一次性地直接设置
__proto__
属性,以便浏览器能更好地优化对象。同时允许通过Object.create(null)
来创建一个没有原型的对象。缺点: 不支持 IE8 以下的版本;这个慢对象初始化在使用第二个参数的时候有可能成为一个性能黑洞,因为每个对象的描述符属性都有自己的描述对象。当以对象的格式处理成百上千的对象描述的时候,可能会造成严重的性能问题。
4.构造函数创建
构造函数创建的方式更多用来在Javascript中实现继承,多态,封装等特性。
function Animal(name) {
this.name = name;
}
let cat = new Animal('Tom');
5.使用工厂模式创建对象 这种方式是使用一个函数来创建对象,减少重复代码,解决了前面三种方式的代码冗余的问题,但是方法不能共享的问题还是存在。
// 使用工厂模式创建对象
// 定义一个工厂方法
function createObject(name) {
var o = new Object();
o.name = name;
o.sayName = function() {
alert(this.name);
};
return o;
}
var o1 = createObject('zhang');
var o2 = createObject('li');
//缺点:调用的还是不同的方法
//优点:解决了前面的代码重复的问题
alert(o1.sayName === o2.sayName); //false
6.class创建
class
关键字是ES6新引入的一个特性,它其实是基于原型和原型链实现的一个语法糖。
class Animal {
constructor(name) {
this.name = name;
}
}
let cat = new Animal('Tom');
设置原型的四种方法
1.通过构造函数的prototype 往原型对象上添加属性
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function(){}
let cat = new Animal('Tom');
cat.eat()
2.通过对象的 __proto__
属性动态设置对象的原型
var a = { n: 1 };
var b = { m : 2 };
a.__proto__ = b;
a.__proto__ === b; // true
优点: 支持所有现代非微软版本以及 IE11 以上版本的浏览器。将
__proto__
设置为非对象的值会静默失败,并不会抛出错误。缺点: 应该完全将其抛弃因为这个行为完全不具备性能可言;干扰浏览器对原型的优化;不支持 IE10 及以下的浏览器版本。
3.通过 Object.create 初始化设置对象的原型
var a = {a: 1};
var b = Object.create(a)
b.__proto__ === a; // true
4.Object.setPrototypeOf 动态设置对象的原型
语法:
Object.setPrototypeOf(obj, prototype)
参数:
参数名 | 含义 |
---|---|
obj | 要设置其原型的对象。 |
prototype | 该对象的新原型(一个对象 或 null ). |
var a = { n: 1 };
var b = { m : 2 };
Object.setPrototypeOf(a, b);
a.__proto__ === b; // true
优点: 支持所有现代浏览器和微软IE9+浏览器。允许动态操作对象的原型,甚至能强制给通过
Object.create(null)
创建出来的没有原型的对象添加一个原型。缺点: 这个方式表现并不好,应该被弃用;动态设置原型会干扰浏览器对原型的优化;不支持 IE8 及以下的浏览器版本。
var a = { n: 1 };
var b = { m : 2 };
a.__proto__ = b;
a.__proto__ === b; // true
三.根据原型来谈谈对象的继承
面向对象编程很重要的一个方面,就是对象的继承。A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。这对于代码的复用是非常有用的。
大部分面向对象的编程语言,都是通过“类”(class)实现对象的继承。传统上,JavaScript 语言的继承不通过 class(ES6 引入了class 语法),而是通过“原型对象”(prototype)实现。那么在JS中常见的继承方式有几种呢?
常见的六种继承方式
方式一、原型链继承
这种方式关键在于:子类型的原型为父类型的实例对象。
//父类型
function Person(name, age) {
this.name = name,
this.age = age,
this.play = [1, 2, 3]
this.setName = function () { }
}
Person.prototype.setAge = function () { }
//子类型
function Student(price) {
this.price = price
this.setScore = function () { }
}
Student.prototype = new Person() // 子类型的原型为父类型的实例对象
var s1 = new Student(15000)
var s2 = new Student(14000)
console.log(s1,s2)
但这种方式实现的本质是通过将子类的原型指向了父类的实例,所以子类的实例就可以通过__proto__访问到 Student.prototype 也就是Person的实例,这样就可以访问到父类的私有方法,然后再通过__proto__指向父类的prototype就可以获得到父类原型上的方法。于是做到了将父类的私有、公有方法和属性都当做子类的公有属性
子类继承父类的属性和方法是将父类的私有属性和公有方法都作为自己的公有属性和方法,我们都知道在操作基本数据类型的时候操作的是值,在操作引用数据类型的时候操作的是地址,如果说父类的私有属性中有引用类型的属性,那它被子类继承的时候会作为公有属性,这样子类1操作这个属性的时候,就会影响到子类2。
s1.play.push(4)
console.log(s1.play, s2.play)
console.log(s1.__proto__ === s2.__proto__)//true
console.log(s1.__proto__.__proto__ === s2.__proto__.__proto__)//true
s1中play属性发生变化,与此同时,s2中play属性也会跟着变化。
另外注意一点的是,我们需要在子类中添加新的方法或者是重写父类的方法时候,切记一定要放到替换原型的语句之后
function Person(name, age) {
this.name = name,
this.age = age
}
Person.prototype.setAge = function () {
console.log("111")
}
function Student(price) {
this.price = price
this.setScore = function () { }
}
// Student.prototype.sayHello = function () { }//在这里写子类的原型方法和属性是无效的,
//因为会改变原型的指向,所以应该放到重新指定之后
Student.prototype = new Person()
Student.prototype.sayHello = function () { }
var s1 = new Student(15000)
console.log(s1)
特点:
- 父类新增原型方法/原型属性,子类都能访问到
- 简单,易于实现
缺点:
- 无法实现多继承
- 来自原型对象的所有属性被所有实例共享
- 创建子类实例时,无法向父类构造函数传参
- 要想为子类新增属性和方法,必须要在
Student.prototype = new Person()
之后执行,不能放到构造器中
方式二: 借用构造函数继承
这种方式关键在于:在子类型构造函数中通用call()调用父类型构造函数
<script type="text/javascript">
function Person(name, age) {
this.name = name,
this.age = age,
this.setName = function () {}
}
Person.prototype.setAge = function () {}
function Student(name, age, price) {
Person.call(this, name, age) // 相当于: this.Person(name, age)
/*this.name = name
this.age = age*/
this.price = price
}
var s1 = new Student('Tom', 20, 15000)
这种方式只是实现部分的继承,如果父类的原型还有方法和属性,子类是拿不到这些方法和属性的。
console.log(s1.setAge())//Uncaught TypeError: s1.setAge is not a function
特点:
- 解决了原型链继承中子类实例共享父类引用属性的问题
- 创建子类实例时,可以向父类传递参数
- 可以实现多继承(call多个父类对象)
缺点:
- 实例并不是父类的实例,只是子类的实例
- 只能继承父类的实例属性和方法,不能继承原型属性和方法
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
方式三: 原型链+借用构造函数的组合继承
这种方式关键在于:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用。
function Person(name, age) {
this.name = name,
this.age = age,
this.setAge = function () { }
}
Person.prototype.setAge = function () {
console.log("111")
}
function Student(name, age, price) {
Person.call(this,name,age)
this.price = price
this.setScore = function () { }
}
Student.prototype = new Person()
Student.prototype.constructor = Student//组合继承也是需要修复构造函数指向的
Student.prototype.sayHello = function () { }
var s1 = new Student('Tom', 20, 15000)
var s2 = new Student('Jack', 22, 14000)
console.log(s1)
console.log(s1.constructor) //Student
console.log(p1.constructor) //Person
这种方式融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。不过也存在缺点就是无论在什么情况下,都会调用两次构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数的内部,子类型最终会包含父类型对象的全部实例属性,但我们不得不在调用子类构造函数时重写这些属性。
优点:
- 可以继承实例属性/方法,也可以继承原型属性/方法
- 不存在引用属性共享问题
- 可传参
- 函数可复用
缺点:
- 调用了两次父类构造函数,生成了两份实例
方式四: 组合继承优化1
这种方式通过父类原型和子类原型指向同一对象,子类可以继承到父类的公有方法当做自己的公有方法,而且不会初始化两次实例方法/属性,避免的组合继承的缺点。
function Person(name, age) {
this.name = name,
this.age = age,
this.setAge = function () { }
}
Person.prototype.setAge = function () {
console.log("111")
}
function Student(name, age, price) {
Person.call(this, name, age)
this.price = price
this.setScore = function () { }
}
Student.prototype = Person.prototype
Student.prototype.sayHello = function () { }
var s1 = new Student('Tom', 20, 15000)
console.log(s1)
但这种方式没办法辨别是对象是子类还是父类实例化
console.log(s1 instanceof Student, s1 instanceof Person)//true true
console.log(s1.constructor)//Person
优点:
- 不会初始化两次实例方法/属性,避免的组合继承的缺点
缺点:
- 没办法辨别是实例是子类还是父类创造的,子类和父类的构造函数指向是同一个。
方式五: 组合继承优化2
借助原型可以基于已有的对象来创建对象,var B = Object.create(A)
以A对象为原型,生成了B对象。B继承了A的所有属性和方法。
function Person(name, age) {
this.name = name,
this.age = age
}
Person.prototype.setAge = function () {
console.log("111")
}
function Student(name, age, price) {
Person.call(this, name, age)
this.price = price
this.setScore = function () {}
}
Student.prototype = Object.create(Person.prototype)//核心代码
Student.prototype.constructor = Student//核心代码
var s1 = new Student('Tom', 20, 15000)
console.log(s1 instanceof Student, s1 instanceof Person) // true true
console.log(s1.constructor) //Student
console.log(s1)
同样的,Student继承了所有的Person原型对象的属性和方法。目前来说,最完美的继承方法!\
方式六:ES6中class 的继承
ES6中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字定义类的静态方法,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
需要注意的是,class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的。
class Person {
//调用类的构造方法
constructor(name, age) {
this.name = name
this.age = age
}
//定义一般的方法
showName() {
console.log("调用父类的方法")
console.log(this.name, this.age);
}
}
let p1 = new Person('kobe', 39)
console.log(p1)
//定义一个子类
class Student extends Person {
constructor(name, age, salary) {
super(name, age)//通过super调用父类的构造方法
this.salary = salary
}
showName() {//在子类自身定义方法
console.log("调用子类的方法")
console.log(this.name, this.age, this.salary);
}
}
let s1 = new Student('wade', 38, 1000000000)
console.log(s1)
s1.showName()
优点:
- 语法简单易懂,操作更方便
缺点:
- 并不是所有的浏览器都支持class关键字
typeof 实现原理
typeof
一般被用于判断一个变量的类型,我们可以利用 typeof
来判断number
, string
, object
, boolean
, function
, undefined
, symbol
这七种类型,这种判断能帮助我们搞定一些问题,比如在判断不是 object 类型的数据的时候,typeof
能比较清楚的告诉我们具体是哪一类的类型。但是,很遗憾的一点是,typeof
在判断一个 object的数据的时候只能告诉我们这个数据是 object, 而不能细致的具体到是哪一种 object, 比如👉
let s = new String('abc');
typeof s === 'object'// true
s instanceof String // true
复制代码
要想判断一个数据具体是哪一种 object 的时候,我们需要利用 instanceof
这个操作符来判断,这个我们后面会说到。
来谈谈关于 typeof
的原理吧,我们可以先想一个很有意思的问题,js 在底层是怎么存储数据的类型信息呢?或者说,一个 js 的变量,在它的底层实现中,它的类型信息是怎么实现的呢?
其实,js 在底层存储变量的时候,会在变量的机器码的低位1-3位存储其类型信息👉
- 000:对象
- 010:浮点数
- 100:字符串
- 110:布尔
- 1:整数
but, 对于 undefined
和 null
来说,这两个值的信息存储是有点特殊的。
null
:所有机器码均为0
undefined
:用 −2^30 整数来表示
所以,typeof
在判断 null
的时候就出现问题了,由于 null
的所有机器码均为0,因此直接被当做了对象来看待。
然而用 instanceof
来判断的话👉
null instanceof null // TypeError: Right-hand side of 'instanceof' is not an object
复制代码
null
直接被判断为不是 object,这也是 JavaScript 的历史遗留bug,可以参考typeof。
因此在用 typeof
来判断变量类型的时候,我们需要注意,最好是用 typeof
来判断基本数据类型(包括symbol
),避免对 null 的判断。
还有一个不错的判断类型的方法,就是Object.prototype.toString,我们可以利用这个方法来对一个变量的类型来进行比较准确的判断
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call('hi') // "[object String]"
Object.prototype.toString.call({a:'hi'}) // "[object Object]"
Object.prototype.toString.call([1,'a']) // "[object Array]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(() => {}) // "[object Function]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(Symbol(1)) // "[object Symbol]"
复制代码
instanceof 操作符的实现原理
之前我们提到了 instanceof
来判断对象的具体类型,其实 instanceof
主要的作用就是判断一个实例是否属于某种类型
let person = function () {
}
let nicole = new person()
nicole instanceof person // true
复制代码
当然,instanceof
也可以判断一个实例是否是其父类型或者祖先类型的实例。
let person = function () {
}
let programmer = function () {
}
programmer.prototype = new person()
let nicole = new programmer()
nicole instanceof person // true
nicole instanceof programmer // true
复制代码
这是 instanceof
的用法,但是 instanceof
的原理是什么呢?根据 ECMAScript 语言规范,我梳理了一下大概的思路,然后整理了一段代码如下
function new_instance_of(leftVaule, rightVaule) {
let rightProto = rightVaule.prototype; // 取右表达式的 prototype 值
leftVaule = leftVaule.__proto__; // 取左表达式的__proto__值
while (true) {
if (leftVaule === null) {
return false;
}
if (leftVaule === rightProto) {
return true;
}
leftVaule = leftVaule.__proto__
}
}
复制代码
其实 instanceof
主要的实现原理就是只要右边变量的 prototype
在左边变量的原型链上即可。因此,instanceof
在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype
,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。
看几个很有趣的例子
function Foo() {
}
Object instanceof Object // true
Function instanceof Function // true
Function instanceof Object // true
Foo instanceof Foo // false
Foo instanceof Object // true
Foo instanceof Function // true
复制代码
要想全部理解 instanceof
的原理,除了我们刚刚提到的实现原理,我们还需要知道 JavaScript 的原型继承原理。
关于原型继承的原理,我简单用一张图来表示
我们知道每个 JavaScript 对象均有一个隐式的 __proto__
原型属性,而显式的原型属性是 prototype
,只有 Object.prototype.__proto__
属性在未修改的情况下为 null 值。根据图上的原理,我们来梳理上面提到的几个有趣的 instanceof
使用的例子。
-
Object instanceof Object
由图可知,Object 的
prototype
属性是Object.prototype
, 而由于 Object 本身是一个函数,由 Function 所创建,所以Object.__proto__
的值是Function.prototype
,而Function.prototype
的__proto__
属性是Object.prototype
,所以我们可以判断出,Object instanceof Object
的结果是 true 。用代码简单的表示一下leftValue = Object.__proto__ = Function.prototype; rightValue = Object.prototype; // 第一次判断 leftValue != rightValue leftValue = Function.prototype.__proto__ = Object.prototype // 第二次判断 leftValue === rightValue // 返回 true 复制代码
Function instanceof Function
和Function instanceof Object
的运行过程与Object instanceof Object
类似,故不再详说。 -
Foo instanceof Foo
Foo 函数的
prototype
属性是Foo.prototype
,而 Foo 的__proto__
属性是Function.prototype
,由图可知,Foo 的原型链上并没有Foo.prototype
,因此Foo instanceof Foo
也就返回 false 。我们用代码简单的表示一下
leftValue = Foo, rightValue = Foo leftValue = Foo.__proto = Function.prototype rightValue = Foo.prototype // 第一次判断 leftValue != rightValue leftValue = Function.prototype.__proto__ = Object.prototype // 第二次判断 leftValue != rightValue leftValue = Object.prototype = null // 第三次判断 leftValue === null // 返回 false 复制代码
-
Foo instanceof Object
leftValue = Foo, rightValue = Object leftValue = Foo.__proto__ = Function.prototype rightValue = Object.prototype // 第一次判断 leftValue != rightValue leftValue = Function.prototype.__proto__ = Object.prototype // 第二次判断 leftValue === rightValue // 返回 true 复制代码
-
Foo instanceof Function
leftValue = Foo, rightValue = Function leftValue = Foo.__proto__ = Function.prototype rightValue = Function.prototype // 第一次判断 leftValue === rightValue // 返回 true 复制代码
总结
简单来说,我们使用 typeof
来判断基本数据类型是 ok 的,不过需要注意当用 typeof
来判断 null
类型时的问题,如果想要判断一个对象的具体类型可以考虑用 instanceof
,但是 instanceof
也可能判断不准确,比如一个数组,他可以被 instanceof
判断为 Object。所以我们要想比较准确的判断对象实例的类型时,可以采取 Object.prototype.toString.call
方法。