原型链
从构造函数开始
构造函数
在ES6之前,对象不是基于类创建的,而是用一种称为构造函数的特殊函数来定义对象和它们的特征。
构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总与new一起使用,我们可以把对象中一些公共的属性和方法抽取出来,然后封装到这个函数里面。
在JS中,使用构造函数时要注意以下两点:
- 构造函数用于创建某一类对象,约定首字母大写
- 构造函数要和new运算符一起使用才有意义
new在执行时会做四件事情:
- 创建一个空的简单 JavaScript 对象(即
{}
); - 为步骤 1 新创建的对象添加属性
__proto__
,将该属性链接至构造函数的原型对象 ; - 将步骤 1 新创建的对象作为
this
的上下文 ; - 如果该函数没有返回对象,则返回
this
(有返回值,返回基本类型则返回this,否则返回该对象)。
手写new运算符【了解后续的原型】
function createNew(){
//解构获得构造函数和参数
const [fn,...params] = arguments
//创建新的空对象
const obj = {}
//链接对象到函数原型
obj['__proto__'] = fn.prototype
//执行构造函数,将构造函数中的this指向obj对象
const result = fn.apply(obj,...params)
//如果函数没有返回对象,就返回新创建的对象
return ((typeof result==='object' && result !==null) || typeof result === 'function') ? result : obj
}
测试createNew
function Person(name){
this.name=name;
Person.prototype.sayName=()=>{
console.log(this.name)
}
}
let a = new Person('king');
a.sayName()
console.log(a)//Person {name: "king"}
let b = createNew(Person,'sara')
console.log(b)//Person {name: "sara"}
静态/实例成员
JavaScript可以在构造函数本身上添加,也可以在构造函数内部的this上添加。
- 静态成员:在构造函数本上添加的成员称为静态成员,只能由构造函数本身来访问
- 实例成员:在构造函数内部创建的对象成员称为实例成员,只能由实例化的对象来访问
function Person(userName){
this.userName = userName;
this.age =20;
this.sing = function(){
console.log("喜欢sing");
}
}
//new实例化
let zhangsan = new Person("张三");
//1.实例成员就是构造函数内部通过this添加的成员,userName age sing就是实例成员
//实例成员只能通过实例化的对象来访问
console.log(zhangsan.userName);
zhangsan.sing();//喜欢sing
console.dir(Person.userName);//undefined,构造函数无法访问实例成员
//2.静态成员在构造函数本身上添加的成员
Person.num = 1234;//静态成员
Person.fn = function(){
console.dir(this);//静态方法里面的this指向构造函数
console.log(this.num)//同时可以访问静态成员
}//静态方法
console.log(Person.num) ;//正确,打印出1234
console.log(zhangsan.num); //undefined,实例化对象无法访问静态成员
Person.fn();//this表示构造函数,数值打印1234
zhangsan.fn();//zhangsan.fn is not a function
构造函数的问题
通过构造函数创建对象:
// 构造函数
function People(name,age) {
this.name = name
this.age = age
this.show = function() {
console.log(`姓名:${this.name},年龄:${this.age}`)
}
}
/*
People是一个普通的函数,这个函数的作用:构造函数
*/
let zs = new People('张三',12)
let ls = new People('李四',13)
/*
每个对象都有自己的show方法,这会很浪费资源,有100个对象就会有100个show方法
*/
console.log(zs.show ===ls.show)//false
如果重复创建多个对象,那么每个对象中的方法都会在内存中开辟新的空间,比较浪费空间
那么如何让show方法不再重复创建呢?
function People(name,age) {
this.name = name
this.age = age
this.show = fun
}
// 全局方法
function fun() {
console.log(`姓名:${this.name},年龄:${this.age}`)
}
let zs = new People('张三',12)
let ls = new People('李四',13)
console.log(zs.show ===ls.show)//true
function fun(){
console.log('重复定义一个fun')
}
ls.show()//重复定义一个fun
定义一个全局fun函数,People内部的this.show指向全局函数fun,因此,我们实例化的zs和ls就实现了共享。
这种方法存在的问题是容易导致全局作用域的污染且数据不安全,容易被覆盖因此,我们需要了解原型。
构造函数继承
call,apply和bind改变this指向
// 构造函数继承
function Dad(name,age) {
this.name = name;
this.age = age;
this.money = "100000";
}
function Son(name,age) {
// Dad.call(this,name,age);
// Dad.apply(this,[name,age]);
Dad.bind(this)(name,age);
this.sex = "男";
}
let zhangsann = new Son("张三",20);
console.log( zhangsann.money);
console.log( zhangsann.sex);
原型以及对象原型
构造函数原型prototype
构造函数通过原型分配的函数是所有对象所共享的。JavaScript规定,每一个构造函数都有一个prototype属性,指向另一个对象。注意这个prototype本身也是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。我们可以把那些不变的方法,直接定义在prototype对象上,这样所有对象的实例就可以共享这些方法。
原型是什么?
一个对象,我们也称为prototype为原型对象。
原型的作用是什么?
共享方法。
对象原型
对象都会有一个属性__proto__
指向构造函数的prototype
原型对象,之所以对象可以使用构造函数prototype
原型对象的属性和方法, 就是因为对象有__proto__
原型的存在。
zhangsan.__proto__ === Person.prototype
function Person(name){
this.name = name;
this.age = 20;
}
// 约定在构造函数里面写属性,在原型上追加方法
// 公共空间原型;
Person.prototype.hobby = function(){
console.log(this.name,"喜欢篮球")
// 原型里面的this和构造函数本身的this相同
}
Person.prototype.fn = function(){
console.log("fn");
console.log(this);
}
let zhangsan = new Person("张三");
let lisi = new Person("李四");
zhangsan.hobby();//调用原型的方法-张三喜欢篮球
lisi.fn();//调用原型的方法-fn
console.dir(Person);//hobby和fn方法在prototype属性里面
console.log(zhangsan.hobby===lisi.hobby);//true
在任何一个构造函数中都有一个prototype对象,而该prototype对象的constructor属性指向prototype对象所在函数。即:
console.log(Person.prototype.constructor === Person) // => true
通过构造函数Person得到的实例对象内部会包含一个指向构造函数的 prototype
对象的指针 __proto__
。
console.log(zhangsan.__proto__===Person.prototype);
所以使得实例对象可以直接访问构造函数中的相关属性以及方法。
总结如下:
任何函数都具有一个 prototype
属性,该属性是一个对象。
构造函数的 prototype
对象默认都有一个 constructor
属性,指向 prototype
对象所在函数。
通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype
对象的指针 __proto__。
所有实例都直接或间接继承了原型对象的成员。
构造函数、实例对象、原型对象三者之间的关系示意图:
通过实例化Object的对象具有同样的特征
let obj = new Object({})
console.log(obj.__proto__ === Object.prototype);// true
console.log(obj.constructor === Object) // true=>相对是因为obj.constructor,最终就是调用的Object.prototype.constructor
原型在使用时可以去追加各种方法,但是不要在原型中直接覆盖属性, 如果在原型中直接写,会覆盖原先的constructor
错误写法:
function Person(name){
this.name = name;
this.age = 20;
}
Person.prototype = {
hobby:function(){
console.log("hobby");
}
}
//原型通过 constructor 指回构造函数
//constructor可以知道是由哪个构造函数创建的,知道对象的指向性问题
let zhangsan = new Person("张三");
console.log(zhangsan.__proto__===Person.prototype);//为true
console.log(zhangsan.constructor===Person);//false
console.log(Person.prototype.constructor===Person);//false
如果需要以上的这种方式将方法追加大量方法到原型需要将constructor指向Person
Person.prototype = {
constructor:Person,
hobby:function(){
console.log("hobby");
}
}
原型链
为什么实例对象可以访问原型对象中的成员:
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。
搜索首先从对象实例本身开始;
- 如果在实例中找到了具有给定名字的属性,则返回该属性的值;
- 如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性;
- 如果在原型对象中找到了这个属性,则返回该属性的值;
- 如果一直到原型链的末端还没有找到,则返回
undefined;
也就是说,在我们调用zhangsan.hobby()的时候,会先后执行两次搜索。
对象之间的继承关系,在JavaScript中是通过prototype对象指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条,称之为原型链。
1.当访问一个对象的属性或方法时,会先在对象自身上查找属性或方法是否存在,如果存在就使用对象自身的属性或方法。如果不存在就去创建对象的构造函数的原型对象中查找 ,依此类推,直到找到为止。如果到顶层对象中还找不到,则返回 undefined。
2.原型链最顶层为 Object 构造函数的 prototype 原型对象,给 Object.prototype 添加属性或方法可以被除null 和 undefined 之外的所有数据类型对象使用。
将test属性添加到Object
// 构造函数
function Foo(name) {
this.name = name;
this.age = 20;
//this.test = "你好111"
}
//原型对象;
Foo.prototype.fn = function () {
console.log("f");
}
//Foo.prototype.test = "你好222";
Object.prototype.test = "你好333";
let newFoo = new Foo("张三");
console.log(newFoo.test);
//测试:依次注释111和222查看打印
//打印newFoo查看关系链
console.log(newFoo)
将test属性添加到实例化对象Foo的prototype
// 构造函数
function Foo(name) {
this.name = name;
this.age = 20;
// this.test = "你好111"
}
//原型对象;
Foo.prototype.fn = function () {
console.log("f");
}
Foo.prototype.test = "你好222";
Object.prototype.test = "你好333";
let newFoo = new Foo("张三");
console.log(newFoo.test);
//测试:将3个你好全部关闭,或者打开部分即可
打印你好222
将test属性添加到构造函数
// 构造函数
function Foo(name) {
this.name = name;
this.age = 20;
this.test = "你好111"
}
//原型对象;
Foo.prototype.fn = function () {
console.log("f");
}
Foo.prototype.test = "你好222";
Object.prototype.test = "你好333";
let newFoo = new Foo("张三");
console.log(newFoo.test);
//测试:将3个你好全部关闭,或者打开部分即可
打印你好111
如果都没有添加,那么就是undefined
原型继承
简单原型继承,出现影响父类的情况(不建议)
function Dad(name,age) {
this.name = name;
this.age = age;
this.money = "100000";
}
Dad.prototype.fn = function(){
console.log("fn");
}
function Son(name,age) {
Dad.call(this,name,age);
this.sex = "男";
}
Son.prototype = Dad.prototype;//原型继承
Son.prototype.fn = function(){
console.log("重写的fn");
}
let zhangsan = new Son("张三",20);
console.log( zhangsan.money);
console.log( zhangsan.sex);
zhangsan.fn();//重写的fn
let zhangyi = new Dad("张一",50);
zhangyi.fn();//重写的fn
//正确继承方式在继承原型后有重新方法就执行重写方法,否则执行原型的方法
组合继承:
function Dad(name,age) {
this.name = name;
this.age = age;
this.money = "100000";
}
Dad.prototype.fn = function () {
console.log("喜欢象棋");
}
function Son(name,age) {
Dad.call(this,name,age);
this.sex = "男";
}
let Link = function(){};
Link.prototype = Dad.prototype;
Son.prototype = new Link();
Son.prototype.constructor = Son;
Son.prototype.fn = function () {
console.log("喜欢篮球");
}
let zhangsan = new Son("张三",20);
console.log( zhangsan.money);
console.log( zhangsan.sex);
zhangsan.fn();//喜欢篮球
let zhangyi = new Dad("张一",50);
zhangyi.fn();//喜欢象棋
深拷贝继承:
function Dad(name,age) {
this.name = name;
this.age = age;
this.money = "100000";
}
Dad.prototype.fn = function () {
console.log("喜欢象棋");
}
function Son(name,age) {
Dad.call(this,name,age);
this.sex = "男";
}
Son.prototype =deepCopy(Dad.prototype);
Son.prototype.fn = function () {
console.log("喜欢篮球");
}
let zhangsan = new Son("张三",20);
console.log(zhangsan.money);
console.log(zhangsan.sex);
zhangsan.fn();//喜欢篮球
let zhangyi = new Dad("张一",50);
zhangyi.fn();//喜欢象棋
传址传值问题
当我们在使用JS进行赋值时,进行简单类型的赋值,只会改变变量的值,而不会改变变量的地址,例如:
// 简单数据类型:传值;新开辟内存(栈)
let a = 10;
let b = a;
b = 20;
console.log(a);
console.log(b);
复杂数据类型传址:
let DadProto = {
name:"张三",
age:20
}
let SonProto = DadProto;
SonProto.name = "李四";
console.log(SonProto);//{ name: "李四", age: 20 }
console.log(DadProto);//{ name: "李四", age: 20 }
大多数情况下我们都不希望这种情况发生,处理这个问题涉及到JS中的深拷贝,JS的深拷贝会为新的变量重新申请一个新的地址块,不会指向原变量的地址,如何实现深拷贝,以下提供了两种方法。
深拷贝
方法一:
JSON.stringify丢失方法和undefined,使用时要注意
let DadProto1 = {
name:"张三",
age:20,
fn() {
console.log("fn..");
},
test:undefined
}
console.log(JSON.stringify(DadProto1));//JSON.stringify丢失方法和undefined,使用时要注意
// 将对象序列化为json字符串,再将字符串转为对象
let SonProto1 = JSON.parse(JSON.stringify(DadProto1));
SonProto1.name = "李四";
console.log(DadProto1);//{ name: "张三", age: 20, fn: fn(), test: undefined }
console.log(SonProto1);//{ name: "李四", age: 20 }
方法二:
// 深拷贝
let obj = {
name:"张三",
age:20,
fn:function() {
console.log("fn..");
},
test:undefined,
arr:[],
}
let obj2 = deepCopy( obj);
obj2.name = "李四";
console.log(obj2);// { name: "李四", age: 20, fn: fn(), test: undefined, arr: [] }
console.log(obj);// { name: "张三", age: 20, fn: fn(), test: undefined, arr: [] }
function deepCopy(obj) {
let newObj = Array.isArray(obj)?[]:{}
for (const key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
if(typeof obj[key] === 'object'){
newObj[key] = deepCopy(obj[key])
}else{
newObj[key] = obj[key]
}
}
}
return newObj
}
hasOwnProperty
hasOwnProperty()
方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)。
所有继承了 Object
的对象都会继承到 hasOwnProperty
方法。这个方法可以用来检测一个对象是否含有特定的自身属性
即使属性的值是 null
或 undefined
,只要属性存在,hasOwnProperty
依旧会返回 true
。
假设有以下对象,判断一些值是否存在于这个对象里
let obj = {a:123}
使用如下的方式只能判断是否存在该属性且为真
if(obj['hello']){
console.log('obj存在hello属性')
}
接下来我们为Object添加hello
Object.prototype.hello = 'hello'
使用if(obj['hello'])
判断将为真,它会查找到原型链的最顶层的hello属性。
使用hasOwnProperty判断
console.log(obj.hasOwnProperty('a'));//ture
console.log(obj.hasOwnProperty('b'));//false
当我们为当前对象的添加hasOwnProperty方法后,就会异常
obj['hasOwnProperty'] = ()=>true
console.log(obj.hasOwnProperty('a'));//ture
console.log(obj.hasOwnProperty('b'));//true
另外一种情况是当前对象或原型上的hasOwnProperty被改写都可能会造成判断错误,而上面对象obj的__proto__
的hasOwnProperty被改写相当于改写了Object.prototype
的hasOwnProperty
obj.__proto__['hasOwnProperty'] = ()=>true
console.log(Object.hasOwnProperty());//true
console.log(Object.hasOwnProperty === Object.prototype.hasOwnProperty);//true
console.log(obj.hasOwnProperty('a'));//ture
console.log(obj.hasOwnProperty('b'));//true
常见判断方式如下,然后以下方式也只能预防obj对象添加的hasOwnProperty
方法
Object.hasOwnProperty.call(obj,'b');//false-理想结果