一、认识对象

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 
};

javascript面向对象封装 js中面向对象_js

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();

此时有一个重大的问题,就是函数都是分别定义在两个实例化对象身上的:

javascript面向对象封装 js中面向对象_面向对象_02


因为两个函数分别位于他们的身上,是两个函数的不同副本:

Xiaoming.sayHello === xioahong.sayHello; //false

2、原型链

每一个构造函数都有一个属性prototype(原型),指向一个对象,当这个构造函数被new的时候,它的每一个实例的_proto_属性,也指向这个对象。

javascript面向对象封装 js中面向对象_构造函数_03


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

javascript面向对象封装 js中面向对象_this_04

五、继承与伪继承

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的继承,但是有个缺点就是,子类继承的属性的属性值都是固定的,若需要用父类的属性,还得重新给属性赋值,这样会变的很麻烦,而且代码量也会增多,所以通过构造器可以更好的实现父类属性的继承,如下示例:

javascript面向对象封装 js中面向对象_this_05


代码中 标橘 色的是通过构造器实现了伪继承,继承的是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