前序热身
一、js中的数据类型
原始类型
a、 原始类型的变量直接保存原始值,(而不是一个指向对象的指针);
var a =1
var b =a
说明:两者的存储地址是不一样的,a变量保存的是值1,b变量保存的也是值1,a的值改变了并不会影响到b;
b、如何鉴别原始类型(typeof 是小写)
console.log(typeof "a"); //"string"
console.log(typeof 10); //"number"
console.log(typeof 7.2);//"number"
console.log(typeof true);//"boolean"
console.log(typeof undefined);//"undefined"
console.log(typeof null); //"object"
引用类型
它其实就是指的是js中的对象,对象是属性的无序列表,属性包含键和值;
- 鉴别引用类型:(instanceof)
var list =[];
var object ={};
function obj (val){
return val
}
console.log(list instanceof Array) //true
console.log(list instanceof Object) //true
console.log(object instanceof Object) //true
console.log(Object instanceof Array) //false
console.log(obj instanceof Function) //true
console.log(obj instanceof Object ) //true
总结:
1、每一种引用类型的对象(数组、对象等)都被正确鉴别为Object的实例
2、如果对象之间存在原型链方式继承关系,比如,对象A继承对象B,那么对象A instanceOf 对象B也会返回true,只要之间存在原型链关联就为true;
- 鉴别数组:Array.isArray(…)
var arr =[];
console.log(Array.isArray(arr)) //true
总结:虽然instanceof()也可以鉴别数组,但是在不同页面之间传递数组时就会出现问题,因为每个页面有自己上下文,当你把一个数组从一个框架传到另一个框夹时,instanceof就无法识别它,所以此时鉴别数组最好用Array.isArray()
二、对象
1、创建对象
- 用new 构造函数的方式创建对象(构造函数就是通过new操作符来创建对象的函数,所有函数都可以是构造函数,一般用首字母大写来区别其余函数)
var object = new Object(); //因为引用类型步在变量中直接保存对象,而是保存一个指向内存中实际对象的地址值
。。。。do something
object.name ="haha" //可以随时给对象添加属性name
object =null //如果这个变量后面不要用了,最好可以将其置为null,让对象引用解除,如果不写也没大事,因为js有垃圾回收机制
- 对象字面形式创建对象
var book ={
name:"三国演义",
year:2013
}
以上这种字面形式定义对象,其实和const book = new Object(); book.name="三国演义"; book.year=2013是一样的;这是js引擎背后做的事情
2、理解对象
- 定义属性:
当一个属性第一次被添加给对象时,js为在对象上调用一个叫 [[put]] 的方法,在对象上创建一个新节点来保存属性,调用[[put]]方法时在对象上创建一个自有属性,自有属性表明仅该指定的对象实例才拥有该属性; - 属性探测:
最好不要用if(Person.name)这种方式来判断对象是否存在该属性,因为当该属性值为null、underfined、0、false、NaN或者空字符时会返回false,但其实际上属性是存在的,所以最好用in标识符来检测某对象中是否含有该属性,如果含有则返回true,它可以检测任何属性(自有属性和原型属性),只要是对象中能访问的属性,in表示符都会返回true,于是就有一个问题?自由属性和原型属性该怎么判断呢?这时就要用到一个hsOwnProperty属性,每个对象都有该属性(方法),该属性只会是自有属性才会返回true,对于原型属性返回false;
例: var Person={
name:'haha',
age:0,
work:'',
a:null,
};
console.log("name" in Person); //true
console.log("age" in Person); //true
console.log("work" in Person); //true
console.log("a" in Person); //true
console.log("c" in Person); //false
console.log("toString" in Person); //true
console.log(Person.hasOwnProperty("toString")) //false,因为toString方法是Object原型中的方法, 不是该对象的自有属性(方法),所以返回false
- 属性删除:
用的了delete操作符进行直接删除,如delete Person.name; js内部可以理解为调用[[delete]]方法;而且要记住,delete操作符只能删除对象自有属性,不能删除原型属性; - 读取对象属性过程:
1、首先会在该对象的自有属性中寻找,如果找到则返回;
2、如果没有,则会通过对象中的—proto—属性所对应的那个对象(该对象的构造函数的原型对象)中去找,如果有则返回,如果没有则继续按原型链往上级寻找,直至到Object.prototype为止,如果还是没有则返回undefined;
三、函数
函数其实也是对象,只不过函数区别于其他对象的一点就是它内部有一个内部属性叫[[call]],该属性是函数独有的,表明该对象可以被执行,
typeof操作符会对任何具有[[call]]属性的对象返回function,所以函数的类型返回就是function;
函数有2种字面形式(函数声明和函数表达式)
- 函数声明:
function add(a,b){
return a+b;
}
- 函数表达式:
const add =function(a,b){
return a+b;
}
注意:函数声明会被js引擎提升至当前上下文(执行环境)的顶部,这个上下文有可能是该函数被声明时所在的函数或者是全局范围,这就意味着你可以提前使用该函数,如下:
const result =add(1,2); //3,如果下面用函数表达式的就会报错
function add(a,b){
return a+b;
}
- 函数的参数问题:
1、arguments:它是一个不是数组,但类似于数组的对象,函数的实参都会传入保存在它里面,所以它的内部值就是调用函数时所传入的实参值;每个实参都用索引值对应存储,就和数组的形式一样,它一般在函数内部使用;
2、length:它表明了此时你定义的函数所期望的参数个数,说白了就是你定义的函数它()中所填写的形参的个数,函数声明形式的length=0,具体以上两个属性的举例见下:
<script>
function add(){
return arguments[2]; //函数返回传入的第3个实参值
}
console.log(add(1, 2, 3,56565),'??????'); //调用add函数,返回值为第三个实参值3
console.log(add.length,'>>>>>>'); //此时add为函数声明形式,没传形参,所以此时 //长度为0
</script>
- 重载:
js语言本身没有重载,如果你定义了两个名字相同的函数,后面的会覆盖掉前面的,这样就意味着同一个函数只能做相同的一件事,但是你可以通过arguments属性来内部做逻辑处理,虚假地搞成重载的样子(同一个函数,输出不一样的内容,当两个不同函数使,利用的就是arguments属性),举个例子:
old:
function sayMessage(message){ //定义一个方法,输出传入的内容
console.log(message);
}
now:
function sayMessage(message){ //还是原先的方法,只不过通过arguments属性做了入参 格式的逻辑判断处理,如果啥也不传,则输出默认信息,如 果有入参,则输出传入的信息
if(arguments.length===0){
message= "Default message"
}
console.log(message);
}
- this对象:每个函数作用域中都存在一个this对象,它代表当前调用该函数的对象;
- 改变this指向(call()、bind()、apply())
1、call():
A.Call(B,x,y) //改变函数A的this指向,将其指向B对象,后面参数x,y也都是需要传入A函数的参数
2、apply():
A.apply(B,x,y) //用法和call完全一样,只不过是入参有一点不一样,接收两个参数,第一个同样是指向当前调用函数内部的this的对象,第二个是一个数组或者一个类似于数组的对象,其内部就是还是那些需要被传入函数的参数,它和call不同的是call都是分散着传入,而他是包在一个数组或伪数组中;
3、bind():
bind()方法会创建一个新的函数,称为绑定函数 ,该方法可传入两个参数,第一个参数作为this,第二个及以后的参数则作为函数的参数调用
例子1:
var test=function(){
console.log(this.x);
}
var o={
x:1
}
test(); //undefined
test.bind(o)(); //1
第一次调用this指向global,第二次test.bind(o)() 这个this指向o,通过观察可以知道test.bind(o)此时是一个函数,说明bind返回的是一个函数。
例子2:
const a = {
b:function(){
var func =function(){
console.log(this.c)
}.bind(this); //将func函数内部的this绑定给()中的这个this,这个this是在b函数中的,那它又指向调用b函数的那个对象,也就是a对象,所以func函数的this最终指向是a对象,所以最终结果是haha
func()
},
c:"haha"
};
a.b(); //"haha"
例子3:
const a = {
b:function(){
var func =function(){
console.log(this.c)
};
func.bind(this)(); //将func函数中的this指向绑定给this,这个this是在函数b中的,也同样指向调用函数b的对象(a)
},
c:"haha"
};
a.b() //“haha”
四、构造函数和原型对象
1、每一个对象在被创建时,都会用一个构造器属性(constructor),指向其构造函数的引用,如果是通过构造函数方式创建的对象,则constructor指向创建它的构造函数,如果是Object构造函数创建或者对象字面量形式创建的对象,该属性指向Object;原型对象的contructor属性也指向其构造函数
const Person =function(name){
this.name=name;
};
const person1 = new Person() ; //通过构造函数Person创建一个对象person1
const person2 ={ //用对象字面量形式创建一个对象person2
name:"123"
};
console.log(person1.constructor ===Person) //true
console.log(person2.constructor ===Object) //true
2、 对象实例与构造函数之间通过原型对象连接:
图解:构造函数中的prototype属性指向其原型对象(Person.prototype),原型对象中的constructor属性指向其构造函数(Person),实例对象中的[[prototype]]属性(_ proto_ )指向其构造函数的原型对象,实例对象能共享器构造函数的原型对象中的所有属性(主要就是一些公用的方法)
3、new 构造函数时内部原理
- new的时候,会立即去执行构造函数的代码,然后在内存中开辟一块新的空间(所以每个实例对象占用的空间是不一样的)
- 在构造函数中创建一个空对象(相当于var kong ={})
- 把构造函数内部的this指向这个空对象(内部相当于this=kong,构造函数中例如this.age=age等语句相当于给new出来的实例对象添加属性或方法)
- 把空对象的内部原型(属性)指向构造函数的原型对象(kong. _ proto_ ==指向构造函数的prototype),这就同等于如果构造函数没有返回值,则new出来的实例对象内部的proto属性就指向了构造函数的原型对象;
- 原型对象中有一个constructor属性回指向构造函数;
- 当构造函数执行完成后,如果有返回值则返回那个return的对象给实例,如果没有,则把当前的空对象返回给实例对象(也就是构造函数内部的this对象就可以理解为有一句默认的影藏代码就是:return this)
示意图:
4、–proto-- 和prototype有什么区别
_proto_是任何对象都有的内部原型属性;而prototype只有函数才有的属性,该属性是在函数创建的时候产生的;
5、其他知识点:
一般在构造函数中添加属性,在原型对象中添加方法,如果有一些常量或者公共的属性也可以设置在原型对象中,这样有助于减少内存,但是说回来还是最好不要设置属性;
原型对象上的属性只能通过原型自己去设置,即通过Person.prototype.name=123,如果实例对象中设置与原型对象中相同的属性,那么他会自动为这个实例对象设置一个属性,不会去改变原型中的值;
6、函数的4种调用模式
①函数执行模式
②对象调用模式
3、构造器调用模式
4、call和apply调用模式
五、继承
1、前言:在其他语言中,继承是通过类来实现的,然而在js中没有类的概念,所以它的继承是发生在没有类继承关系的对象之间,这种继承机制叫做原型对象链,又称原型对象继承;
2、什么叫原型对象链:对象实例继承其原型对象中的属性方法,原型对象继承它的原型对象的属性方法,一次类推就是原型对象链;所以所有对象都继承Object.prototype
3、Object.prototype中的方法:
- hasOwnProperty()检查对象是否存在一个给定名字的自有属性
- propertyIsEnumerable()检查一个自有属性是否可枚举
- isPrototypeOf()检查一个对象是否是另一个对象的原型对象
- valueOf()返回一个对象的值表达(每当一个操作符用于对象上时,此时对象就会调用该方法)
- toString()返回一个对象的字符串表达,js引擎在大部分情况下默认返回[object,Object],如果你对该返回值不满意,可以在对象中自定义重写toString方法
4、常见的几种继承方式:
-
对象继承
①、自定义的字面量形式对象,会默认继承Object.prototype;这是隐式继承;
②、但是你也可以显式地继承,通过用Object.create(),该方法接收两个参数,第一个是你要该创建的新对象继承的原型对象,第二个是这个新对象的一些原生属性表达;
理解:
说白了什么叫对象继承,const A =Object.create(B),就是说将一个对象A的_proto_属性设置为另一个对象B,这样对象A就相当于是某个构造函数创建出来的实例对象,对象B就相当与是某个构造函数的原型对象,对象A就继承了对象B中的属性和方法,这样就相当于实现了继承,这种继承就叫做对象继承;那这是通过什么来实现的呢,就是通过Object.create()方法;
var person1 ={ //定义一个对象person1 name:"haha", sayName:function(){ ... } } var person2 =Object.create(person1,{ //通过该方法将person2的原型对象设置为person1即(person2._proto_指向person1) name:{ //在继承person1的同时还定义了一个自己的属性name,值为Greg configurable:true, enumerable:true, value:Greg, writable:true } }) person1.sayName(); //"haha" person2.sayName(); // "Greg" //自己有name属性就用自己的 console.log(person1.hsaOwnProperty('sayName')) //true console.log(person1.isPrototypeOf(person2)) //true console.log(person2.hsaOwnProperty('sayName')) //false,这个方法不是person2的 自有属性,是继承person1的,所以为false
-
类式继承
function A (name){ this.name=name; this.list= [1,2,3] } A.prototype.getName=function(){ console.log(this.name) } function SubA (name){ this.subName = 'sub' + this.name } SubA.prototype = new A(); const sa1 = new SubA('sa1') console.log(sa1.name,sa1.list) //undefined [1,2,3]
代码解析:首先执行的是new A(),这个上面讲过了会创建一个新对象,然后执行A构造,将A构造函数中的this指向它,所以这个新对象中就有name属性和list属性{name:undefined,list:[1,2,3]}这里name为undefined是因为在new的时候没传name值,然后该对象的_proto_属性指向A.prototype,所以此时该对象中也同时拥有了getName方法,{name:undefined,list:[1,2,3],getName:function(){this.name}},然后将这个对象赋值给了SubA.prototype,所以SubA.prototype ={name:undefined,list:[1,2,3],getName:function(){this.name}},然后代码继续执行,执行new SubA(‘sa1’),此时会执行SubA构造函数,同样又会创建一个空对象obj2,该对象拥有subName属性,然后该对象obj2.proto_又同样会指向SubA构造函数的prototype,所以该对象obj2={ subName:‘sub sa1’, _ proto :{name:undefined,list:[1,2,3],getName:function(){this.name}}},执行结束后,返回该对象obj2,赋值给了实例对象sa1,所以sa1= { subName:‘sub sa1’, _ proto _:{name:undefined,list:[1,2,3],getName:function(){this.name}}},所以此时打印sa1.name,sa1.list就是undefined [1,2,3]
类式继承的缺点:这种方法不支持父构造函数带参数,还有就是父构造函数中属性和方法都会变成共有属性,可能原先设计的父构造函数中的属性和方法并不想共有,所以这种方式有些不太合适
-
构造函数继承
function A (name){ this.name=name; this.list= [1,2,3] } A.prototype.getName=function(){ console.log(this.name) } function SubA (name){ A.call(this,name); this.subName = 'sub' + this.name; } const sa1 = new SubA('sa1') console.log(sa1.name,sa1.subName) //sa1 subsa1 console.log(sa1.getName()) //sa1.getName is not a function
代码解析 :首先执行new SubA(‘sa1’),它会去执行SubA构造函数,构造函数内部执行了A.call(this,name);又会去执行A构造函数,它A构造函数中的this指向SubA构造函数中创建出来的对象objSubA,所以此时objSubA={name:“sa1”,list:[1,2,3]},执行完A构造函数后,继续执行this.subName = ‘sub’ + this.name;往objSubA对象上继续赋对象,所以此时objSubA={name:“sa1”,list:[1,2,3],subName:‘sub sa1’},然后执行完SubA构造函数后,将objSubA赋值给了sa1实例对象,所以此时打印name和subName分别为sa1 和subsa1,由于该对象上没有getName方法,所以执行sa1.getName()会报错
构造函数继承问题:它不能继承父构造函数原型对象上的方法和属性
-
组合继承
function A (name){ this.name=name; this.list= [1,2,3] } A.prototype.getName=function(){ console.log(this.name) } function SubA (name){ A.call(this,name); this.subName = 'sub' + this.name; } SubA.prototype = new A(); const sa1 = new SubA('sa1') console.log(sa1.name,sa1.subName) //sa1,sub sa1 console.log(sa1.getName()) //sa1
代码解析:这种方式其实就是结合了类式继承和构造函数继承,首先执行SubA.prototype = new A();此时会执行A构造函数,创建一个对象objA, _ proto_属性指向A.prototype,所以该对象就有了name:undefined和list:[1,2,3],_ proto_:{getName:function(){console.log(this.name)}} 然后将该对象赋值给了SubA.prototype,然后执行new SubA(‘sa1’),同样的方式执行SubA构造函数,创建一个新的对象objSubA,然后执行A.call(this,name);将name和list属性赋值在了objSubA对象上,此时objSubA={ name: ‘sa1’,list:[1,2,3]},然后继续执行this.subName = ‘sub’ + this.name;,此时objSubA={ name: ‘sa1’,list:[1,2,3]’‘,subName:’sub sa1‘}然后objSubA对象内部属性_proto_ 同样会指向SubA.prototype,所以此时sa1=objSubA={ name: ‘sa1’,list:[1,2,3]’‘,subName:’sub sa1‘ ,_ proto_:name:undefined和list:[1,2,3],_ proto_:{getName:function(){console.log(this.name)}} },所以此时打印sa1.name和subName,由于sa1实例对象本身有name和subName,所以就输出sa1和sub sa1,getName()自身没有,要通过_ proto_属性向上找,最后输出sa1
组合继承的问题:这种方式也是有两个小问题,第一就是_proto_中的属性没有被用到,第二就是父构造函数被调用了2次
-
寄生式组合继承
function A (name){ this.name=name; this.list= [1,2,3] } A.prototype.getName=function(){ console.log(this.name) } function SubA (name){ A.call(this,name); this.subName = 'sub' + this.name; } // SubA.prototype = new A();要改善的就是这一句,这一句太过于强硬 function inheritPrototype(subClass,superClass){ function F(){} F.prototype=superClass.prototype; subClass.prototype= new F(); } inheritPrototype(SubA,A); const sa1 = new SubA('sa1') console.log(sa1.name,sa1.subName) //sa1,sub sa1 console.log(sa1.getName()) //sa1
代码解析:执行inheritPrototype(),内部创建一个F函数,F.prototype = A.prototype, 然后执行new F(),又创建了一对象,其_proto_ =F.prototype,所以该对象为{proto:A.prototype},然后将该对象赋值给了SubA.prototype,然后执行new SubA(‘sa1’),又创建了一个新对象objSubA,然后该对象就拥有了name,subName,list属性,然后该对象的_proto_指向SubA.prototype,所以最后sa1实例对象最后={name:“sa1”,subName:“sub sa1”,list:[1,2,3],proto :{proto:A.prototype}}