一、认识对象
1、对象在JS中一般大致分为两种
- 一种是指自己创建的对象,就是用{}这种字面量的形式定义的对象,它是一组属性的无序集合,格式为 key:value ,比如以下代码:
var obj = {
name : "小明",
age : 24,
sex : "女",
hobby : ["追剧","买买买","狗狗"]
}
- 另一种是系统内置的所有引用类型值,JavaScript 中的所有事物都是对象:字符串、数字、数组、日期,等等。在 JavaScript 中,他们是拥有属性和方法的数据,如下代码:
var arr = [132,13,1314,435,35];
alert(typeof arr); //object
//数组有自己内建的方法和属性,比如:
alert(arr.length); //内建的属性,输出5
alert(arr.sort())//内建的方法,输出13,1314,132,35,435,此代码没有按照数值的大小对数字进行排序,要实现这一点,就必须使用一个排序函数
//内置的对象除了有自己内建的方法外,还可以自定义添加属性和方法(但有些内置对象是不能自定义添加属性和方法的,例如数值、字符串),例如:
arr.name = "小名";
arr.sex = "女";
arr.hobby = function(){
alert("测试");
}
alert(arr.name); //小名
arr.hobby();//测试
2、对象和JSON的区别
JSON就是javascript object notation,JS对象表示法。JSON是JS对象的严格子集。
在写法上区别就是引号,JSON要求所有的键必须加双引号,而JS对象实际上不要求双引号。
在解析层面上:二者看起来都是数据,而且恰巧又都是文本;不同的地方在于,JS字面量的文本是被脚本引擎直接解析的,而JSON的文本,如果要转化为JS对象的话,是交给eval函数来处理的,那么,如何理解JSON的文本,就取决于这个函数,而不是脚本引擎,因为这2者的处理根本就不在一个层面上。
//js对象的字面量表示法:
var people1={
name:'hehe',
age:18
};
//json的格式是:
var people1={
"name":'hehe',
"age":18
};
3、对象的方法
如果一个对象的属性值,是一个函数,我们称这个属性叫做这个对象的方法。
var obj = {
name : "xiaming",
age : 23,
sex : "女",
sayHello : function () {
alert("你好");
}
}
obj.sayHello();//弹出你好
那么现在函数的this是谁?
当一个函数被当做对象的方法调用的时候,这个函数里面的this表示这个对象,因此上面sayHello函数被调用后,内部的this指向obj。
但是如果这个函数不是被当做对象的方法调用的时候,此时的this就不是obj了,如下:
var obj = {
xingming : "小明",
age : 23,
sex : "女",
sayHello : function () {
alert("你好,我叫做" + this.xingming + "!我的年龄是:" + this.age);
}
}
var fn = obj.sayHello; //拿出了这个函数
fn();//直接圆括号调用!不是对象打点调用!此时的this指向window
二、函数的this
1、五种情况下的this
(1)函数用圆括号直接调用,函数this是window对象
function fun() {
var a = 100;
alert(this.a);
}
var a = 200;
fun(); //弹出200,函数直接圆括号调用,此时this指的是window对象
函数function fun(){}的this是什么?不要看是怎么定义的,要看是怎么调用的!此时是fun()函数名加上圆括号直接调用,此时上下文就是window对象!
所有的全局变量都是window对象的属性(注意:函数里面的局部变量,不是window的属性,不是任何事物的属性,它就只是一个变量 )
(2)函数如果作为一个对象的方法,通过对象打点调用,函数的this就是这个对象
如下示例:
function fun() {
var a = 100;
alert(this.a);//相当于弹出obj.a
}
var obj = {
"a" : 10,
"b" : 20,
//给这个对象添加一个方法,值就是fun函数
"c" : fun
}
var a = 200;
//我们要看清楚函数执行的时候,是怎么执行的
//现在是一个对象打点调用这个函数,所以函数的this是obj对象!
obj.c();//弹出10
调用的时候,是对象.函数(),所以函数里面的this是这个对象,所以能够弹出10。
(3)函数是事件处理函数,函数this就是触发这个事件的对象
如下示例:
function fun() {
this.style.background = "red";
}
var box1 = document.getElementById("box1");
var box2 = document.getElementById("box2");
var box3 = document.getElementById("box3");
//把同一个函数绑定给不同的对象
//此时点击谁,this就是谁
box1.onclick = fun;
box2.onclick = fun;
box3.onclick = fun;
(4)定时器调用函数,上下文是window对象
function fun() {
alert(this.a);
}
var a = 888;
setInterval(fun,1000);//函数fun被定时器调用,此时函数的this就是window对象。每秒钟能弹出888
备份this
因为定时器里的this是window对象,但如果在下面的点击事件的定时器里改变被点击的样式,就需要在定时器外提前备份this(当前被点击的元素)如下示例:
var box1 = document.getElementById("box1");
box1.onclick = function () {
var self = this;//备份this
setTimeout(function () {
self.style.background = "red";
})
}
在定时器外面的事件处理函数中,this就是box1这个元素,此时我们可以备份this。我们把this存为局部变量self,后面的程序就用self指代box1,还可以用_this、that等等。
(5)数组中存放的函数,被数组索引调用,this就是这个数组
如下示例:
function fun() {
alert(this.length);
}
var arr = [fun,"21","13"];
var length = 10;
arr[0]();//3,因为数组长度3,被数组索引调用,this就是这个数组,this.length就是这个数组的长度
2、arguments
arguments.callee.length就是形参列表的个数。
arguments.length表示实参个数,就是调用函数的时候传进来的实参个数。
如下示例:
function fun(a,b,c,d,e,f) {
console.log(arguments.callee.length); //打印出6
console.log(arguments.length); //打印出3
}
fun(1,2,3);
3、call()和apply()
这两个函数都是函数的方法,只有函数能够打点调用call()、apply(),表示用指定的调用者(上下文)执行这个函数。
下面有一个函数fun,我们现在必须让xun里面的this是obj,此时可以用call、apply来指定上下文。
语法:
函数引用.call(调用者,参数1,参数2…)
函数引用.apply(调用者,[参数1,参数2…])
两个函数的区别,体现在参数上,此时我们要执行的fun函数可以接受参数,apply需要用数组体现这些参数,而call必须用逗号隔开,罗列所有参数
function fun(a,b,c) {
alert(a+b+c);
alert(this.age);
}
var obj = {
"name" : "小名",
"age" : 24
}
fun.call(obj,6,7,8);
fun.apply(obj,[6,7,8]);
三、构造函数
上一篇已经列举了非常多的调用函数的方式:
- 圆括号直接调用
- 对象打点调用
- 定时器调用
- 事件处理函数调用
- 数组枚举调用
他们体现的不同点,就是函数的调用者不同,this不同。
下面有一种新的函数调用方法,用new运算符调用函数。
1、new运算符
如下示例:
function fun() {
this.name = "小红";
this.age = 24;
this.sex = "男";
}
var obj = new fun();
alert(obj.age); //24
通过new调用函数,这个函数的this是谁?
当通过new调用这个函数的时候,函数会创建一个对象,而this就是指新创建的这个对象,最后会return这个对象,并返回this。
用new操作符,可以返回具有相同属性群的对象:
//构造函数
function People(name,age,sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
var xiaoming = new People("小明",12,"男");
var xiaohong = new People("小红",11,"女");
var xiaoqing = new People("小青",12,"男");
console.log(xiaoming);
console.log(xiaohong);
console.log(xiaoqing);
我们可以认为People是一个“类”,xiaoming、xiaohong、xiaoqing都是这个类的“实例”
当一个函数被new操作符调用的时候,这个函数就是一个构造函数,他总能返回一类具有相同属性群的对象,感觉在“构造东西”,像一个模子,在制作类似的对象。
为了提醒其他程序员,这是一个用new调用的函数,换句话说提醒别人这是一个构造函数,这类函数的名字最好首字母大写。
Javascript不是一个面向对象的语言,它只是基于对象。
2、构造函数
构造函数,它就是一个普通函数,只不过用new来调用了。用 new 关键字来调用的函数,称为构造函数。
而构造函数里面不一定是this.*** = ***的语法,可以写任何语句,如下代码:
//构造函数就是一个普通函数,里面可以写任何语句!!
//只不过这个函数里面的this表示新创建的空对象
//我们可以通过this.*** = ***给这个对象赋值
function Dog(name,pinzhong,color,age,sex,nation) {
this.name = name;
this.pinzhong = pinzhong;
if(this.pinzhong == "金毛" || this.pinzhong == "藏獒" ||this.pinzhong == "斑点狗" ){
this.type = "大型犬";
}
else if(this.pinzhong == "柯基" || this.pinzhong == "秋田"){
this.type = "中型犬";
}
else if(this.pinzhong == "泰迪" || this.pinzhong == "萨摩" ||this.pinzhong == "土狗"){
this.type = "小型犬";
}
else{
this.type = "未知体型";
}
this.color =color;
this.age = age;
this.isChengnian = this.age >= 8 ? true : false;
this.sex = sex;
this.nation = nation;
}
var snoopy = new Dog("史努比","斑点狗","白色",3,"公","美国");
console.log(snoopy);
一个构造函数不用写return就能自动帮你返回一个对象,但是,如果写了return,会怎么样?
- 如果return这个基本类型值,则无视这个return,该return什么还是return什么,但是return阻止了构造函数的执行;
function fun(name,age) {
this.name = name;
return 3;//
this.age = age;
}
var xiaoming = new fun("小明", 12);
console.log(xiaoming);//不是3,3被忽视了,但是打断了程序的执行,因此输出fun{name:"小明"}
类似的基本类型值如下:
return "addwed";
return false;
return true;
return NaN;
return undefined;
return null;
- 如果return了引用类型值,则原有return被覆盖。
function fun(name,age) {
this.name = name;
return {"a" : 1, "b" : 2};
this.age = age;
}
var xiaoming = new fun("小明", 12);
console.log(xiaoming);//返回{a:1,b:2}
类似的引用类型值如下:
return {};
return [211,32];
return /\d/;
return Math;
return function;
return document;
四、原型与原型链
1、给构造函数添加方法
之前我们只定义属性,没有定义方法,事实上构造函数可以定义方法:
function People(name,age,sex) {
this.name = name;
this.age = age;
this.sex = sex;
this.sayHello = function () {
alert("你好我是" + this.name + "! 我的年龄是" + this.age);
}
}
//实例化两个对象
var xiaoming = new People("小明",19,"男");
var xiaohong = new People("小红",18,"女");
//对象打点调用他们的方法
xioaming.sayHello();
xioahong.sayHello();
此时有一个重大的问题,就是函数都是分别定义在两个实例化对象身上的:
因为两个函数分别位于他们的身上,是两个函数的不同副本:
Xiaoming.sayHello === xioahong.sayHello; //false
2、原型链
每一个构造函数都有一个属性prototype(原型),指向一个对象,当这个构造函数被new的时候,它的每一个实例的_proto_属性,也指向这个对象。
People.prototype是People构造函数的“原型”,People.prototype是小明的“原型对象”
_proto_有原型链查找功能。当xiaoming身上没有某个属性的时候,系统会沿着_proto_去寻找它的原型对象身上有没有这个属性。
示例如下:
function People(name,age,sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
People.prototype = {
"teacher" : "考拉",
"zhishang" : 180,
"gongzi" : 30000
}
//实例化一个对象
var xiaoming = new People("小明",19,"男");
console.log(xiaoming.teacher);//实例化出来的对象去原型对象上寻找teacher属性
3、方法的定义在原型上
我们刚才知道如果把函数写在构造函数里面,就相当于定义在了对象身上
此时就会有以下两个缺点:
1、 每次创建一个实例,函数就会产生一个不同副本,每个副本中的方法都是一样的逻辑,如果创建很多,就有可能造成系统内存泄露,从而引起性能下降。实际上方法只需一个就够了。
2、 使得方法中的局部变量产生闭包:闭包会扩大局部变量的作用域,使得局部变量一直存活到函数方法之外的地方,有可能产生数据安全问题。
此时可以把函数定义在构造函数的原型上,这样所有new出的对象,_proto_就会指向这个对象,从而能够使用这个方法。
function People(name,age,sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
People.prototype.sayHello = function () {
alert("你好我是" + this.name + "! 我的年龄是" + this.age);
}
//实例化三个对象
var xiaoming = new People("小明",19,"男");
var xiaohong = new People("小红",18,"女");
xiaoming.sayHello();
xiaohong.sayHello();
alert(xiaoming.sayHello() === xiaohong.sayHello());//true
五、继承与伪继承
1、继承
将父类的实例给子类的原型就可实现js的继承,如下实例:
function Person(name,age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function () {
console.log(this.name + "向您打招呼");//输出:牛魔王向您打招呼
}
var per = new Person("牛魔王" , 22);
per.sayHello();
function Student(grade) {
this.grade = grade;
}
//将Student的prototype设为Person对象(也就是把new出的一个Person实例赋值给了Student.prototype),因此Student就有了Person的属性name和age,并且对应的值是“未命名”和0
Student.prototype = new Person("未命名",0);
Student.prototype.intro = function () {
console.log("%s是一个学生,读%d年级",this.name,this.grade);
}
var stu = new Student(5);
stu.name = "孙悟空";//因为此时Student的name和age的值是固定的“未命名”和0,所以要想变成Student自己的值,就需要重新赋值。
console.log(stu instanceof Student);//输出true
console.log(stu instanceof Person);//输出true,说明确实继承了父类Person
stu.sayHello();//输出:孙悟空向您打招呼
stu.intro();//输出:孙悟空是个学生,读5年级
2、构造器实现伪继承
虽然把父类的实例给子类的原型就可实现js的继承,但是有个缺点就是,子类继承的属性的属性值都是固定的,若需要用父类的属性,还得重新给属性赋值,这样会变的很麻烦,而且代码量也会增多,所以通过构造器可以更好的实现父类属性的继承,如下示例:
代码中 标橘 色的是通过构造器实现了伪继承,继承的是Person的属性,也就是 标紫 的部分;而 标红 的是子类Student真正继承了父类Person(将父类的实例给了子类,包括属性和方法),但因为属性继承的效果不好,所以这里只用这种方法来继承方法,就是 标蓝 的部分。
3、使用apply或call简化伪继承
通过以上两种继承方法配合起来基本就可以完美的实现了继承了,但通过构造器实现伪继承的代码(标橘色的部分)还可以更简单,就是通过apply或者call来实现。
为什么可以用call和apply来实现?
因为构造器的伪继承关键在于子类构造器需要以this作为调用者来调用父类构造器,这样父类构造器中的this就会变成代表子类,子类就可以得到原父类定义的实例属性和方法,因此这种伪继承方式完全可以用apply和call来实现,只要在使用apply或call调用时指定this作为调用者即可。
即对象冒充,在子类中把本身(this)作为调用者调一下父类,这样父类的属性的this就指向了子类本身了。
因此把上面示例标橘色的代码换成以下代码就可以了:
Person.call(this,name,age,sex);
//或者
Person.apply(this,[name,age,sex])
这样就是一个标准的“类”的继承了~
六、创建对象
创建对象基本有三种方法,前两种是经常使用的方法,最后一种只做了解。
1、第一种方法:使用new关键字调用构造器创建对象
这是面向对象语言创建对象的方式,如下代码:
function Person(name,sex,age) {
this.name = name;
this.sex = sex;
this.age = age;
}
var p1 = new Person("小明" ,"女", 5)
2、第二种方法:使用JSON语法创建对象
JSON语法提供了一种更简单的方式来创建对象,使用JSON语法可避免书写函数,也可避免使用new关键字,可以直接创建一个js对象。示例如下代码:
var child = {
name : "小红",
sex : "女",
info : function () {
alert(this.name);
},
schools : ['小学', '中学' ,'大学'], //可以有数组
parents : [ //可以有数组对象
{
name : 'father',
age : 60,
address : '广州'
},
{
name : 'mother',
age : 58,
address : '深圳'
}
]
}
3、第三种方法:使用Object直接创建对象
Js对象都是Object类的子类,因此可以采用如下代码创建对象:
var myObj = new Object();
myObj.name = 'xioa';
myObj.age = 13
4、第一种创建方法和第二种创建方法结合使用
一般很多情况下第一种和第二种方法要结合起来使用,会更方便,如下代码:
function Person(name,sex,age,childname,childsex) {
this.name = name;
this.sex = sex;
this.age = age;
this.child = {
name : childname,
sex : childsex
}
}
var p1 = new Person('ming','boy',12 , 'hong' , 'girl');
console.log(p1.child.name);//输出hong