原型与原型链
一、构造函数
理解:构造函数也是函数,只是和new关键字连用了
目的:就是为了创建一个有属性,有方法,合理的对象
使用注意事项:
- 调用必须有
new关键字,可以帮助你自动创建一个对象;否则就没有创建对象的能力
// 定义一个函数
function fn () {}
// 与new关键字连用,输出一个对象
const o1 = new fn()
console.log(o1); // fn {} -> [[Prototype]]: Object
// 不与new关键字连用,输出undefined
const o2 = fn()
console.log(o2); // undefined
-
在构造函数内部不要写
return,因为会造成以下两种情况:- 如果
return一个基本数据类型,那么这个return不起作用 - 如果
return一个复杂数据类型,那么会返回这个复杂数据类型,导致构造函数失去了本身的意义
- 如果
// 定义一个函数
function fn1(){
// return 一个基本数据类型
return 123
}
// 这里会得到一个空对象,return不起作用
const o3 = new fn1()
console.log(o3); // {}
function fn2(){
// return 一个复杂数据类型
return []
}
// 这里会得到一个空数组,那么就失去了构造函数的意义
const o4 = new fn2()
console.log(o4); // []
- 在调用构造函数的时候,如果不传递参数,那么后面的
()可以不写,但是建议写上
// 定义一个函数
function fn3() {}
// 调用构造函数带()
const o5 = new fn3()
console.log(o5); // {}
// 调用构造函数不带()
const o6 = new fn3
console.log(o6); // {}
// 输出结果相同
- 构造函数推荐首字母大写,为了区分与普通函数的区别
function A() {
this.a = 'Tom'
this.b = '18'
}
const a1 = new A()
console.log(a1); // A {a: 'Tom', b: '18'}
function a() {
var a = 100
var b = 200
return a + b
}
const a2 = a()
console.log(a2); // 300
// 以上,可以更直观的看出构造函数与普通函数的区别
- 当函数和
new关键字连用之后,把创建出来的对象叫做实例化对象,创建对象的过程,叫做实例化,构造函数的this指向当前实例对象
function Person(name) {
this.name = name
this.sayHi = function () { console.log('hello!'); }
}
// 实例化对象p1,this指向p1
const p1 = new Person('Jack')
// 实例化对象p2, this指向p2
const p2 = new Person('Rose')
p1.sayHi() // hello!
p2.sayHi() // hello!
console.log(p1, p2); // p1: Person {name: 'Jack', sayHi: ƒ}; p2: Person {name: 'Rose', sayHi: ƒ}
- 当构造函数中存在方法的时候,
new多少次,就会开辟出多少个空间用来存放一模一样的方法,浪费了内存空间
function Person1 (name) {
this.name = name
this.sayHi = function () { console.log('你好 世界'); }
}
const per1 = new Person1('小明') // 开辟空间存储 sayHi方法
const per2 = new Person1('小红') // 开辟空间存储 sayHi方法
console.log(per1, per2);
// 最后导致占用了两块内存存储一样的方法
二、prototype(原型 / 原型对象)
说明:
- 每一个函数天生自带一个
prototype属性,它是一个对象 - 这个
prototype对象天生自带一个属性constructor,它表示是由哪一个构造函数伴生的原型对象
作用:prototype的作用就是为了书写一些方法给实例对象使用的,因为在prototype身上的方法每一个实例对象都可以访问,详看三
结论:在原型上书写方法,可以给多个实例使用
// 定义一个构造函数
function Food() {}
// 往函数原型对象上添加方法
Food.prototype.sayHi = function() { console.log('hello!'); }
// 只要是函数,天生自带就自带prototype属性
// sayHi: ƒ (); constructor: ƒ Food()
// sayHi: ƒ ();表示被添加的方法
// constructor: ƒ Food() 表示是由Food这个函数伴生出来的constructor
console.log(Food.prototype);
- 每一个对象天生自带一个属性
__proto__,指向所属构造函数的prototype
// 定义一个构造函数
function Food1() {}
// 给构造函数的原型对象上添加方法
Food1.prototype.sayHi = function() { console.log('hello!'); }
// 实例化一个对象
const foo1 = new Food1()
// 函数天生自带的prototype原型对象
console.log("Food1.prototype:",Food1.prototype);
// 对象天生自带的__proto属性
// 指向所属构造函数的prototype
console.log("foo1.__proto__:",foo1.__proto__);
const result = Food1.prototype === foo1.__proto__
// 它俩比较输出结果相等,说明它们指向同一个空间地址
console.log(result); // true
三、对象访问机制
解释:当访问一个对象成员的时候,先去对象本身上找;如果没有,会去对象的__proto__上找;如果还没有,那就再去下一个__proto__上找;一直找下去,直到顶级对象的__proto__;如果还没有,那么就返回undefined
使用:利用prototype和__proto__和对象访问机制,解决了构造函数需要创建多次一样的方法这个问题;也就是说,可以在构造函数内部创建属性,在构造函数的prototype身上创建方法,就可以解决这个问题
function Food2(name, age) {
this.name = name
this.age = age
}
// 在构造函数的prototype身上添加方法
Food2.prototype.sayHi = function() { console.log('hello!'); }
// 创建foo2实例对象
const foo2 = new Food2('jack', 18)
foo2.sayHi() // hello
// 创建foo3实例对象
const foo3 = new Food2('rose', 20)
foo3.sayHi() // hello
// 因为对象访问的时候首先会去实例对象foo3本身找,发现没有,会去对象的__proto__上找,而foo3的__proto__指向Food2的prototype,所以就找到了sayHi这个方法了
四、构造函数的this指向
构造函数体内的this,和构造函数prototype身上的this,都指向当前实例
function Test (name) {
this.name = name
console.log('构造函数的this:',this); // Test
}
Test.prototype.fn = function () {
console.log("构造函数prototype上的this:", this); // Test
}
// 与 new 连用,this 指向当前实例 test
const test = new Test('jack')
// 调用 fn,this 指向 test
test.fn()
五、原型链
定义:
使用__proto__串联起来的对象链状结构,叫做原型链
作用:为了对象访问机制服务,就是为了访问成员
- 每一个函数天生自带一个属性
prototype,它是一个对象 - 每一个对象天生自带一个属性
__proto__,它指向所属构造函数的prototype - 当一个对象没有准确的构造函数来实例化的时候,都看作是内置构造函数
Object的实例 - 任何一个对象从
__proto__开始向上查找,最终都能找到*Object.prototype*(顶级原型对象)
var arr = [] // Array 的实例
var obj = {} // Object 的实例
var time = new Date() // Date 的实例
var fn = function () {} // Function 的实例
var p1 = new Person() // Person 的实例
// 没有准确的构造函数
Person.prototype // Object 的实例
Array.prototype // Object 的实例
扩展:
- 如果想要给数组,对象等扩展方法,可以在它们的
prototype身上写方法,调用的时候直接数组.方法就可以调用 constructor属性(构造器),只有函数天生自带的那个prototype上才有,表示是哪一个构造函数所自带的原型对象,可以用它来判断复杂数据类型
console.log([].constructor === Array); // true
console.log({}.constructor === Object); // true
六、判断数据类型
-
typeof判断数据类型- 准确判断基本数据类型,对于复杂数据类型的判断并不准确
// 基本数据类型
console.log(typeof '123'); // string
console.log(typeof 123); // number
console.log(typeof true); // boolean
console.log(typeof undefined); // undefined
// null会被判断成 object 类型,是浏览器的 bug
console.log(typeof null); // object
// 要注意 NaN 的判断
console.log(typeof NaN); // number
// 复杂数据类型除了函数会被判断出 function 类型外,其他都会判断成 object 类型
console.log(typeof function () {}); // function
console.log(typeof {}); // object
console.log(typeof []); // object
console.log(typeof new Date); // object
-
constructor判断复杂数据类型- 利用原型的属性,利用访问机制
- 判断不出来
null和undefined
const num = 123
console.log(num.constructor); // Number()
console.log('123'.constructor); // String()
console.log(true.constructor); // Boolean()
console.log([].constructor); // Array()
console.log({}.constructor); // Object()
console.log(new Date().constructor); // Date()
console.log((function () {}).constructor); // Function()
console.log(null.constructor); // 报错
console.log(undefined.constructor); // 报错
-
instanceof- 对象
instanceof构造函数 - 检测不了基本数据类型
- 对象
console.log({} instanceof Object); // true
console.log([] instanceof Array); // true
console.log((function () {}) instanceof Function); // true
console.log((new String()) instanceof String); // true
// 检测不了基本数据类型
console.log('' instanceof String); // false
-
Object.prototype.toString.call('你要检测的数据类型')- 可以准确检测数据类型,包含基本数据类型和复杂数据类型
数字.toString()// 数字数组.toString()// 1,2,3对象.toString()// [object Object]
console.log(Object.prototype.toString.call(123))[object Number]
console.log(Object.prototype.toString.call(''))[object String]
console.log(Object.prototype.toString.call(true))[object Boolean]
console.log(Object.prototype.toString.call(null))[object Null]
console.log(Object.prototype.toString.call(undefined))[object Undefined]
console.log(Object.prototype.toString.call(new Date()))[object Date]
console.log(Object.prototype.toString.call(function(){}))[object Function]
console.log(Object.prototype.toString.call({}))[object Object]
console.log(Object.prototype.toString.call([]))[object Array]
七、数据劫持
function Lenovo () {
this.name = 'Jack'
this.age = 18
this.gender = '男'
}
Lenovo.prototype.sayHi = function () { console.log('hello world'); }
const len1 = new Lenovo()
console.log(len1);
-
for in循环-
遍历对象身上所有的可枚举的属性
- 自定义属性
- 设置为可枚举属性
-
// 循环可枚举属性
for (let key in len1) {
console.log(key); // name age gender sayHi
}
-
hasOwnProperty()- 查看是自己的属性还是原型链上的属性
对象.hasOwnProperty('要检测的属性名')
// 检测对象身上的方法
console.log(len1.hasOwnProperty('name')) // true
// 因为是原型上的方法,所以为false
console.log(len1.hasOwnProperty('sayHi')) // false
-
defineProperty()数据劫持- 作用:可以实现双向数据绑定,不用直接操作
dom结构 - 给对象添加属性的方法,可以给自己设置的属性设置各种各样的行为状态(能不能修改,是不是可枚举……)
Object.defineProperty(<给哪一个对象添加>,'key',{ 添加的设置 })
- 作用:可以实现双向数据绑定,不用直接操作
// 创建一个对象
const obj1 = {
name : 'jack'
}
// 普通添加
obj1.age = 18
// 数据劫持
Object.defineProperty(obj1, 'gender', {
value: '男', //配置gender成员的值
enumerable: true // 是否可枚举,默认是不可枚举,如果设置为true会打印出gender
})
obj1.name = 'rose'
obj1.age = 20
obj1.gender = '女'
console.log(obj1); // rose, 20, 男(gender属性不可以修改,因为是使用数据劫持设置的)
console.log(obj1);
for(let key in obj1) {
console.log(key); // name age -> 说明gender被数据劫持了,不可枚举所以打印不出来
}
get()set()
const obj1 = {
name : 'jack'
}
// 普通添加
obj1.age = 18
// 数据劫持
Object.defineProperty(obj1, 'gender', {
// 使用get()返回值的形式,属于劫持来的属性,通过get()计算得来的
get() {
return '男'
}
})
obj1.name = 'rose'
obj1.age = 20
obj1.gender = '女' // gender属性不可以修改,因为是使用数据劫持设置的
console.log(obj1); // rose, 20, 男
const obj1 = {
name : 'jack'
}
// 定义一个对象
const prop = {
firstName: '张',
lastName: '思锐'
}
// 数据劫持
Object.defineProperty(obj1, 'username', {
get() {
return prop.firstName + prop.lastName
},
// 想要修改劫持的数据时会触发这个函数
set(val) { // val 接收要修改的值
console.log('修改', val); // 每次想要修改都会触发
// 对要修改的值进行处理
let a = val.slice(0,1)
let b = val.slice(1)
prop.firstName = a
prop.lastName = b
// 在dom中写一个输入框
let inp = document.querySelector('#username')
// 把输入框的值设置为要修改的值
inp.value = prop.firstName + prop.lastName
}
})
// 修改username为 qwer
obj1.username = 'qwer'
// 实现双向数据绑定
const inp = document.querySelector('#text')
inp.addEventListener('input', () => {
obj1.username = inp.value
})