一. JS的对象
1.1 创建对象的几种方式
1.1.1 通过字面量创建对象
在js中,一对{}
其实就是一个对象
var person = {
name: "tom",
age: 23,
read: function () {
console.log(name, ": read book")
}
}
1.1.2 通过系统的构造函数
通过系统的构造函数创建一个空的对象,然后用js动态语言的特性,如果一个对象没有某个属性或者方法,那么我们点一下再附上值就好了
var person2 = new Object()
person2.name = "jerry"
person2.age = 23
person2.say = function () {
console.log(person2.name, ": say hello")
}
1.1.3 通过自定义构造方法
自定义构造方法一般都是首字母大写的函数
function Person(name, age, sex) {
this.name = name
this.age = age
this.sex = sex
this.say = function () {
console.log(this.name, " :say hello")
}
}
// 创建对象时,使用 new 关键字
p = new Person("tom", 23, "man")
console.log(p instanceof Person)
自定义的构造方法创建对象,会经历如下几个步骤
- 开辟空间
- 将this设置成当前对象
- 初始化属性和方法
- 将this返回
1.1.4 工厂模式创建对象
function Person(name,age,sex) {
// new Object 作为当前的返回值
var obj = new Object()
obj.name = name
obj.age = age
obj.sex = sex
obj.say = function () {
console.log(this.name," :say hello")
}
// 手动将对象返回出去
return obj
}
// 工厂模式创建对象,不需要使用new 关键字
var p = Person("tom",23,"man")
console.log(p instanceof Person) // false
1.2 构造函数与实例对象
看下面的例子:
// 构造函数和实例的关系
function Person(name) {
this.name = name
this.say = function () {
console.log(this.name," :say hello")
}
}
// 对象p是通过 自定义的构造函数Person创建出来的
var p = new Person("tom")
console.dir(p)
console.dir(Person)
打印的结果如下:
- 实例对象的
__proto__
属性中有constructor属性,上面记录着自己的构造方法。 - Person是构造方法,也是对象,我们直接打印Person得到的结果中有个属性prototype,它里面也有个属性叫做 constructor。里面记录着构造方法就是自己本身。
- 结合上面的例子,我们其实可以得到这样的推断,实例对象的原型属性 和 构造函数的原型属性中的constructor都指向了同一个构造方法 ,然后可以进一步推断
p是Person类型
。
__prototype__实际上就是原型对象,在下文中会详细的说
还是上面的例子,看如下的输出也就能理解了
console.log(p.constructor === Person) // true
console.log(p.__proto__.constructor == Person) // true
console.log(p.__proto__.constructor == Person.prototype.constructor) // true
// 由此推断出,p === Person
console.log(p instanceof Person) // true
其实有个小问题,看上面代码的第一行console.log(p.constructor === Person)
我们通过上面的代码也看不到实例对象p constructor属性啊,怎么就能用,也不报错undefined呢?
其实这就是牵扯到js对象的原型链了,(下面的章节会说),总的来说,就是js的对象会优先使用构造方法中的属性和方法,如果构造函数中不存在我们使用的属性和方法的话,就尝试去这个对象所对应的构造方法中的原型对象中的属性和方法,再没有就会报错。
二. JS的原型
2.1 引入原型的必要性
为什么会突然再来看js的原型呢?
因为看到了vue的源码中,大量的方法都被添加再vm的原型上,所以,回顾一下原型肯定是躲不过去了。
一般我们使用原型就是为了节省空间。
想理解节省了什么空间? 那就看看下面这个不节省空间的例子。
// 构造函数创建对象带来的问题
function Person(name) {
this.name = name
this.say = function () {
console.log(this.name,": say hello")
}
}
var p1 = new Person("tom")
var p2 = new Person("jerry")
p1.say() // tom : say hello
p2.say() // jerry : say hello
// todo 返回false, 表示说,p1和p2的say方法,并不是同一份, 其实这并不是一件好事
console.log(p1.say == p2.say)
上面的p1 和 p2 都是通过一个构造函数创建出来的不同对象,他们里面都有say这个函数,当我们输出 p1.say == p2.say
时,返回了false,说明每个对象中都有一份say方法,那假设有1000个对象,岂不是就有1000个say方法了? 这肯定是浪费空间的。
那么有没有办法可以让每次new出来的对象都使用一份say方法呢?
当然,如下:
// 共享函数,引出原型
function Say() {
console.log(this.name, ": say hellp")
}
function Person(name) {
this.name = name
this.say = Say
}
var p1 = new Person("tom")
var p2 = new Person("jerry")
p1.say()// tom : say hellp
p2.say()// jerry : say hellp
// 这样的话,确实能实现节省空间,但是容易出问题
console.log(p1.say == p2.say) // ture
现在确实实现了我们的需求,但是不够优雅,而且统一出现问题,js是动态类型的语言,那我们像下面这样,假设不知道已经有Say这个函数了,然后将var Say = "hello"
放置在第Say函数之后,就会产生覆盖。
2.2 认识原型
看下的例子:我们往构造方法的原型对象上添加一个say方法。
其实这块也不是不好理解,你想啊,js的对象通过构造方法创建出来,我们把公共的方法,属性放在构造方法的原型对象中,是不是就可以让他们共享这些方法和属性呢?
function Person(name) {
this.name = name
}
// 在原型上添加方法
// 为什么可以说原型是对象呢? 想想js中一个对象可以通过 点 , 动态点添加属性和方法?
Person.prototype.say = function () {
console.log(this.name,":say hello")
}
var p1 = new Person("tom")
var p2 = new Person("jerry")
p1.say()//tom :say hello
p2.say()//jerry :say hello
console.log(p1.say == p2.say) // true
通过console.dir()
打印下上面的实例对象和构造函数,得到如下图:
console.dir(p1)
console.dir(p2)
console.dir(Person)
通过上图可以看到,可以得到下面的结论:
- 实例对象中的直接拥有的标准属性,比如name, 这些都是直接出现在构造方法中的属性,而且这些属性是js对象所私有的。
- 上图中实例对象有个属性叫做:
__proto__
, 这个属性是用来给浏览器使用的,而不是给程序员使用,所以我们称它为非标准属性。 此外谷歌浏览器是支持这个属性的,但是在IE8浏览器中,我们执行这句console.log(p1.__proto__)
会报错,说undefined
2.3 原型,实例对象,构造函数之间到底是什么关系呢?
-
实例对象是通过 new 构造函数创建出来的,所以构造函数是创建实例对象的模版。
-
构造函数就是那个首字母大写的函数,在js里面我们能直接
console.log(构造函数)
因为这个构造函数其实也是个对象。 -
原型的作用我们说了,就是为了将公共的方法抽取出来,全部存放在构造函数的原型对象中,而实现数据的共享,节省内存。
-
实例对象的·
__proto__
, 是个非标准属性,也是个对象,这个对象指向了 构造方法的prototype
属性。 -
构造方法的
prototype
属性是个标准属性, 同时也是个对象,我们对通过构造方法.prototype.属性/方法 = XXX
的方式为其添加属性和方法。 -
我们通过
对象.属性/方法
时, 会优先从对象的构造方法中查找,如果找不到的会再尝试从原型中查找,这也是为什么会出现一个明明没有为一个对象添加相应的属性或者方法但是对象却能点出来,并且能正常使用。当然如果原型中也不存在的话,就会报错说 undefined
2.4 关于this对象
- 看下面的第一个例子
下面出现的this并不难理解, 就是我们new 出来的对象本身
function Person(name) {
// 考虑一下,这个this是谁?
this.name = name
console.log(this)
}
var p = new Person("tom")
- 看下面的第二个例子
我们在构造方法的原型对象上添加一个方法say,在这say方法中使用的this对象指的同样是 我们new 出来的对象本身。即方法的调用者。
function Person(name) {
// 考虑一下,这个this是谁?
this.name = name
console.log("n10: ",this)
}
Person.prototype.say = function () {
// todo 这里的this指的是谁呢?
// 首先,方法是添加在原型对象上, 那么this指的是原型对象吗?
// 通过控制台可以看到,this.name ,其实不是原型对象,而是say()方法的调用者(实例对象)
console.log("n16: ",this.name,": say hello")
}
var p1 = new Person("tom")
var p2 = new Person("jerry")
p1.say()
p2.say()
- 看下面的第三个例子:
下面在给构造方法的原型对象添加方法时,不仅出现了this, 还出现了that。
this对象依然是我们手动new出来的对象本身。
that同样是指向了我们new出来的对象本身,之所以需要中转一下,是因为在按钮的点击事件里面,this指向的是按钮本身。
// 用面向对象的方式封装构造函数
function ChangeStyle(btnId, dvId, color) {
this.btnObj = document.getElementById(btnId)
this.dv = document.getElementById(dvId)
this.color = color
}
// 在构造方法的原型上添加方法
ChangeStyle.prototype.init = function () {
// 这里面的this表示的是 调用init方法的实例对象
var that = this
this.btnObj.onclick = function () {
// todo 为什么原型中的函数中,就不能使用this,而是that呢???
// todo 或者问下,当前函数中的this是谁呢?
that.dv.style.backgroundColor = that.color
}
}
2.5 其他原型的写法
- 最常见的写法就是像下面这样,在当前原型的基础上添加属性或者方法
function Person(name) {
this.name = name
}
// 前面的例子中我们都是像下面这样写代码, 这其实是对原来的 原型对象属性的累加
// 原来的原型对象中有个属性,叫做consturctor
Person.prototype.say = function(){
//todo
}
- 也可以像下面这样
这样设置原型的话,实际上是对原来的原型对象的覆盖,所以说需要像下面这样重新添加constructor的指向。
当然我也试了一下,如果说覆盖原来的原型对象,且不添加contructor的指向,我们使用 instanceof 判断实例对象是否是对应的构造函数类型时,还是能得到正确的结果。
Person.prototype = {
constructor:Person, // 手动修改构造器的指向
height:"20",
weight:"20",
say:function () {
// todo
}
}
2.6 方法之间的相互访问
- 构造函数中的成员方法是可以相互访问的。
function Person(name) {
this.name = name
this.say = function () {
console.log("say")
// 通过这个例子,可以看到,对象的方法中可以直接调用对象的方法
this.eat()
}
this.eat = function () {
console.log("eat")
}
}
- 原型中的方法也是可以相互访问的。
function Person(name) {
this.name = name
}
Person.prototype.say = function(){
console.log("say")
// 原型中的方法也可以相互访问
this.eat()
}
Person.prototype.eat = function(){
console.log("eat")
}
var p1 = new Person("tom")
p1.say()
2.7 覆盖内置对象原型中的方法
像这样就可以实现对原型中的方法进行覆盖的操作。
当然可以通过在原型上添加方法实现对原有封装类的拓展。
// 在现有的js封装类上干这件事,也算是在修改源码
String.prototype.myReverse = function () {
for (var i = 0; i < this.length; i++) {
console.log("发生倒叙")
}
}
var str = "123"
str.myReverse()