首页 > 技术文章 > 003_JS基础_面向对象基础

gja1026 2018-01-26 19:11 原文

3.1 对象

  引入:在js中表示一个人的信息(name, gender, age)通过var申明三个变量,但是这样使用基本数据类型的变量,他们是互相独立的,没有联系;
  此时就需要使用对象,对象是一种复合数据类型,在对象中可以保存多个不同数据类型的属性;
对象的分类

  1. 內建对象:有ES标准中定义的对象,在任何的ES的实现中都可以使用,例如:Math, String, Boolean, Function, Object……;
  2. 宿主对象:由js的运行环境提供的对象,目前来讲是浏览器提供的对象,例如:BOM, DOM;
  3. 自定义对象:由开发人员自己创建;

对象的基本操作

  1. 创建对象:使用new关键字调用的函数,是构造函数Constructor,构造函数是专门用来创建对象的函数;使用typeof检查,返回object;语法 var obj = new Object();
  2. 向对象中添加属性,语法:对象.属性名 = 属性值;
  3. 读取对象的属性,语法:对象.属性名;
  4. 修改对象的某个属性值,语法:对象.属性名 = 新的属性值;
  5. 删除对象的某个属性,语法:delete 对象.属性名;
//创建一个对象
var obj = new Object();

//为该对象添加属性
obj.name = "frank";
obj.gender = "male";
obj.age = 20;

/* 
 * 读取对象的属性:
 * 格式: 对象.属性名
 * 注意,如果读取对象中没有的属性,不会报错,会返回undefined;
 */
console.log(obj.name);
console.log(obj.hello);

/*
 * 修改对象的属性值:
 */
console.log(obj.name); //修改前
obj.name = "小明";
console.log(obj.name); //修改后

/*
 * 删除对象的属性
 * 格式: delete 对象.属性名
 */
delete obj.name;

属性名和属性值
  属性名:属性名不强制要求标识符的规范,可以自己随便取,但是我们尽量按照标识符的命名要求,如果实在作死,取了奇怪的命名,例如纯数字,则不能使用“.”来对我们的对象进行基本操作,应该使用这种模式: 对象["属性名"] = 属性值;

/*
 * 奇怪的属性名处理情况,不能使用.操作符
 * 有时使用[]来操作对象更加的灵活,[]中可以传一个变量,此时变量值就是会读取的对应的属性名;
 */
var obj2 = new Object();
obj["123"] = 100;
obj["name"] = "小明";

//定义一个变量,值为变量名:
var x = "name";

console.log(obj["123"]); //输出:100
console.log(obj[x]); //输出:小明

  属性值:可以是任意的数据类型,甚至也可以是一个对象;
  in 运算符,可以检查一个对象中是否含有指定的属性;有则返回true,否则返回false;

var obj = new Object();
var obj2 = new Object();

obj2.course = "语文";

obj.name = "小明";
obj.score = obj2; //属性值还可以是对象

//使用in运算符检查
console.log("age" in obj); //false
console.log("name" in obj); //true

引用数据类型
  JS中的变量都是存储到栈内存中的,基本数据类型的值都是在栈内存中存储,值与值之间是独立存在的,修改一个变量不会对其他变量产生影响;对象(即引用数据类型)是存储到堆内存 中,每创建一个新的对象,在堆内存中就会开辟一个新的内存空间,而变量保存的是对象的内存地址(对象的引用),如果两个变量保存的是一个对象的引用,当通过一个对象修改属性值时,另一个也要受到影响;

对象字面量

/*
 * 使用字面量来创建对象
 * 使用这种方式可以在创建时直接指定属性
 * 对象的属性名,引号可以加可以不加;若名字特殊,一定要加引号;
 */
var obj = {
	name: "Frank",
	age: 20,
	"gender": "男",
	test:{
		name: "小明",
		age: 15	
	}
};
console.log(obj.test.age); //输出15

对象的方法
  函数也可以成为对象的属性,此时这个函数是这个对象的方法,调用函数则称为调用对象的方法

var obj = new Object();
	
obj.name = "frank";
obj.age = 18;
//将一个匿名函数赋值给一个对象的属性
obj.showInformaton = function(){
	console.log("姓名:" + obj.name + ", 年龄:" + obj.age);
}

obj.showInformaton();
	
/*
 * 另一种写法
 */
var obj2 = {
	name: "Alice",
	age: 20,
	showName: function(){
		console.log(obj2.name);
	}
};
obj2.showName();

枚举对象中的属性
  如何将一个对象中的所有属性列出来?

/*
 * 使用for……in语句
 */
var obj = {
    name: "frank",
	gender: "男",
	age: 21,
	hometown: "四川"
};
		
//将对象中的属性名读取出来
for(var n in obj){
	console.log("属性名:" + n);
}
//将对象中的各个属性值读取出来(注意不能使用“.”运算符)
for(var n in obj){
	console.log(obj[n]);
}
//综合一下
for(var n in obj){
	console.log(n + ":" + obj[n]);
}

3.2 函数

  函数也是对象,函数中可以封装一些功能,在需要时可以执行这些功能;即函数可以保存一些代码,在需要时调用;使用typeof检查一个函数对象时,将返回function;

定义函数

  • 方法一:
    • 创建一个函数对象,可以将要封装的代码以字符串的形式传递给构造函数;
    • 函数也是一种对象,它拥有普通对象的所有功能;
//创建一个函数对象
var fun = new Function("alert('Hello')");

//调用函数:函数对象+();
fun();
//可以给该函数对象添加一个属性
fun.name = "function";
  • 方法二:
    • 这种方式较为常用,使用函数声明来创建一个函数;
/* 
    使用函数声明来创建一个函数,格式如下
	function 函数名([形参1,形参2, ……]){
		函数体;
	}
 */
 //创建一个函数:
function hello() {
	alert("创建函数的第二种方法");
}
//调用函数:函数名+();
hello();
  • 方法三:
    • 使用函数表达式来创建一个函数;
    • 这种方式的实质是声明了一个匿名函数,并将其复制给了一个变量;
/*
    使用函数表达式来创建一个函数
    var 函数名 = function([形参1,形参2, ……]){
  		语句;
    }
 */
//创建函数:
var fun3 = function() {
	alert("这是匿名函数中封装的代码");
};
//调用函数:变量+ ();
fun3();

函数的参数
  在调用函数时,可以在()中指定实参(实际参数),实参将会赋值给函数内部对应的形参(形式参数),在调用函数时不会检查实参的类型,所以要注意,是否有可能接收到非法参数;在调用函数时,解析器也不会检查实参的数量,多余的实参不会被赋值,若少了,没有对应的实参,该形参则是undefined;

函数的返回值
   使用return后的值会作为函数的执行结果返回;在函数中,return后的语句不会被执行;

/*
 * 求三个值的和
 */
function sum(a, b, c){
	return a + b + c;
}

var result = sum(1, 2, 3);
console.log(result);

函数的返回值可以是任意数据类型,可以是对象,也可以是一个函数。

/*
 * 函数的返回值可以是任意类型
 * 可以是对象
 */
 function fun(){
	 var obj = {name: "frank"};
	 return obj;
 }
 console.log(fun().name);
		
 var a = fun();
 console.log(a);
 console.log(a.name);
		
/*
 * 函数的返回值也可以是函数
 */
 function fun2(){
	function fun3(){
		alert("Fun3");
		}

	//注意区别 fun3(),是返回函数的返回值
	//fun是返回函数对象本身

	//return fun3();
	return fun3;
 }
		
 var b = fun2(); //将fun2的返回值(一个函数对象)赋值给b
 console.log(b);
 b(); //调用fun3函数
		
 //也可以直接这样调用
 fun2()();

立即执行函数
  函数定义完之后,立即被调用,这种函数就是立即执行函数;特点:这种函数往往只能执行一次;

/*
 * 立即执行函数:
 * 将一个匿名函数使用()包括起来;
 * 函数调用: 函数对象后加();
 */
 (function(){
	 alert("这是一个匿名函数!");
 })();
		
/*
 * 一个带参数的立即执行函数
 */
 (function(a, b){
	 console.log(a + "+" + b + "=" + (a+b));
 })(1, 2);

作用域
  作用域是指一个变量的作用范围,在JS中一共有两种作用域;
1. 全局作用域
  直接编写在script标签中的js代码,都在全局作用域;全局作用域在页面打开时创建,在页面关闭时销毁;全局作用域中的变量都是全局变量;
  在全局作用域中,有一个全局对象window,它代表着浏览器窗口,由浏览器创建,我们可以直接使用;
  在全局作用域中,创建的变量都会作为window对象的属性保存;创建的函数都会作为window对象的方法;

var a = 10;
console.log(a);
console.log(window.a);

function fun(){
	console.log("fun");
}

fun();
window.fun();

  变量声明提前
  使用var关键字声明变量时,会在所有的代码执行之前被声明(但是不会赋值),如果不使用var关键字,则变量不会被声明提前;
  函数声明提前
  使用函数声明形式创建的函数function 函数名(){ },它会在所有的代码执行前就被创建,所以函数可以在函数声明前的位置处被调用; 使用函数表达式创建的函数不会被声明提前:var a = function(){};

2. 函数作用域(局部作用域)
  调用函数时创建函数作用域,函数执行结束后,函数作用域销毁;每调用一次函数就会创建一个新的函数作用域,他们之间相互独立;
  在函数作用域中,可以访问到全局作用域的变量; 在全局作用域中,不能访问到函数作用域的变量;
  就近原则:当在函数作用域中操作变量时,先在自身作用域中寻找,若有则使用,没有则在上一级作用域中去寻找,直到找到全局作用域中,若全局作用域中也没有,则会报错:ReferenceError;
  在函数中,若要访问全局作用域中的变量,使用window.变量名;
  在函数作用域中也有声明提前这个特性;   在函数中不使用var声明的变量,则是一个全局变量;
  形参就相当于在函数作用域中声明了变量;

this
  解析器在调用函数时,每次都会向函数中传递一个隐含的参数,这个隐含的参数就是this,this指向的是一个对象,这个对象我们称之为函数执行的上下文对象(当前对象);
  根据函数的调用方式不同,this会指向不同的对象;

  1. 以函数的方式调用时,this永远都是window;
  2. 以方法的形式调用,this就是调用方法的那个对象;
function fun(){
    console.log(this.name);
}
		
var obj = {
	name: "frank",
	showName: fun
}
var obj2 = {
	name: "小猪佩奇",
	showName: fun
}
//以方法的形式调用
obj.showName();
obj2.showName();
		
var name = "全局";
//以函数形式调用
fun();

使用工厂方法创建对象

function createPerson(name, age, gender){
//创建一个新对象
var obj = new Object();
向对象中添加属性
obj.name = name;
obj.age = age;
obj.gender = gender;
obj.showName = function(){
	console.log(this.name);
    }
//返回该对象
return obj;
}
		
var obj1 = createPerson("frank", 18, "男");
var obj2 = createPerson("kitty", 20, "女")
		
obj1.showName();
obj2.showName();

使用工厂方法创建的对象使用的构造函数都是Object,这种方式不常用;

构造函数
   创建一个构造函数,专门用来创建Person对象,构造函数其实就是一个普通的函数,创建方式和普通函数一样,但是习惯上我们对构造函数的函数名会采取首字母大写

  • 构造函数和普通函数的调用方式不同,普通函数直接调用,构造函数需要使用new关键字来调用;
  • 构造函数的执行流程:
    1. 会立即创建一个新的对象
    2. 将新建对象设置为函数的this,在构造函数中可以使用this来引用新建的对象
    3. 逐行执行函数的代码
    4. 将新建的对象作为返回值返回
  • 使用同一个构造函数创建的对象称为一类对象,也将构造函数称为一个类,我们将通过一个构造函数创建的对象,称之为该类的实例;
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
this.showName = function() {
	console.log(this.name);
	};
};

function Dog(name, age) {
	this.name = name;
	this.age = age;
	this.sayHello = function() {
		console.log("I can say 'hello'");
	};
}

//将对象实例化
var p1 = new Person("Frank", 18, "男");
var p2 = new Person("Alice", 20, "女");
var d1 = new Dog("Tom", 3);

p1.showName();
d1.sayHello();
/*
 * 使用instanceof可以检查一个对象是否是一个类的实例
 */
console.log(p1 instanceof Person);

  目前我们使用的方法都是在构造函数内部创建的,例如方法showName(),也就是说,构造函数每执行一次,就会创建一个新的showName()方法,就是说每一个实例的showName()方法都是唯一的,执行10000次就会创建10000个方法,而这10000个都是一模一样的,这是完全没有必要,我们完全可以使所有的对象使用同一个方法;

原型对象
   我们所创建的每一个函数,解析器都会在函数中创建一个prototype,这个属性对应着一个对象, 这个对象就是我们称为的原型对象;

  1. 如果函数作为普通函数调用,prototype无作用;
  2. 如果函数以构造函数的形式调用时,它所创建的对象都会有一个隐含的属性,指向该构造函数的原型对象;我们可以通过__proto__(两根下划线)来访问该属性;

  原型对象就相当于一个公共的区域,所有同一个类的实例都可以访问到这个原型对象,我们就可以通过这个特点,将对象中所共有的内容,统一设置到原型对象中。
  当我们访问对象的属性或者方法时,先在对象本身中寻找,若没有则去原则对象中寻找,以后我们在创建构造函数时,可以将这些对象的公共的属性和方法,统一添加到构造函数的原型对象中,这样不用为每一个对象都添加,也不会影响到全局作用域,便可以使每一个对象都具有这些属性和方法了。

function MyClass() {

}

//向MyClass的原型中添加属性
MyClass.prototype.a = 123;

//向MyClass的原型中添加方法
MyClass.prototype.sayHello = function() {
	console.log("Hello");
}

var mc = new MyClass();
mc.a = "mc的属性";
var mc2 = new MyClass();

console.log(MyClass.prototype);
console.log(mc.__proto__ == MyClass.prototype); //true
console.log(mc2.__proto__ == MyClass.prototype); //true

//访问对象的属性或方法(先对象本身,在原型对象)
console.log(mc.a); //"mc的属性"
console.log(mc2.a); //123
mc.sayHello(); //“Hello”

//使用in运算符检查
//先检查对象本身,在检查原型对象中,若其中一个含有则返回true
console.log("a" in mc); //true

//可以使用对象的hasOwnProperty()来检查对象自身中是否含有某个属性
console.log(mc.hasOwnProperty("age")); //false
console.log(mc.hasOwnProperty("a")); //true
console.log(mc2.hasOwnProperty("a"));// false

  原型对象也是对象,所以它也有原型:当我们在使用一个对象的属性或者方法时,会先在自身中寻找,自身中如果有则使用,如果没有则去原型对象中寻找。如果原型对象中有,则使用,如果在没有就去原型的原型中寻找;直到找到Object对象的原型,Object对象的原型没有原型(null),如果在Object中依然没有找到,则会返回undefined;

//寻找hasOwnProperty()方法
console.log(mc.__proto__.__proto__.hasOwnProperty("hasOwnProperty");//true

toString()
  我们直接在页面中打印一个对象时,实际上是输出的toString()方法的返回值;
  如果我们希望在输出对象时不输出[Object Object],可以为对象添加一个toString()方法;

//给对象std添加一个toString()方法:
std.toString = function(){			
    return "给对象添加了一个toString()方法"; 	
};

//修改Person原型的toString():
Student.prototype.toString = function(){
	return "姓名:" + this.name + ", 性别:" + this.gender + ", 年龄:" + this.age;
}

垃圾回收

  • 当一个对象,没有任何的变量或者属性对它进行引用,此时我们将永远无法操作该对象,此时这种对象就是一个垃圾;这种对象过多会占用大量的内存空间,导致程序运行变慢;
  • 在JS中有自动垃圾回收机制,会自动将这些垃圾对象从内存中销毁;
  • 我们需要做的是将不再使用的对象设置为null即可,如果不设置obj为null,则该对象还存在变量对其进行引用,该对象不会被自动销毁;

函数的方法
  call()和apply(),这两个方法都是函数对象的方法,需要通过函数对象来调用,当对函数调用call()和apply()两个方法时,都会调用函数执行;

  • 在调用call()和apply()方法时,可以将一个对象指定为第一个参数,此时这个对象会成为函数执行时的this;
  • call()方法可以将实参放在第一个参数(某个对象)之后依次传递;
  • apply()需要将实参封装到一个数组中统一传递;
function test(a, b){
 	console.log(this);
 	console.log("a = " + a);
 	console.log("b = " + b);
 }
 var obj3 = new Object();
 
 test();
 
 //apply()方法需要把实参封装到数组中
 test.call(obj3, 1, 2);
 //apply()方法需要把实参封装到数组中
 var arr = [4, 5];
 test.apply(obj3, arr);

总结一下this

  1. 以函数形式调用,this永远都是window;
  2. 以方法的形式调用,this是调用方法的对象;
  3. 以构造函数的形式调用时,this是新创建的那个对象;
  4. 使用call()或者apply()调用时,this是第一个参数中指定的对象;

arguments
  在调用函数时,浏览器每次都将传入两个参数:

  1. 函数的上下文对象this
  2. 封装实参的对象 arguments
    • arguments是一个类数组对象,它可以通过索引来操作数据,也可以获取长度;
    • 在调用函数时,我们所传的实参都会在arguments中保存;
    • arguments.length可以获取实参的个数;
    • 注意:我们即使不定义形参,我们也可以通过arguments来使用实参,arguments[0] 表示第一个实参;
    • arguments中有一个属性callee, 这个属性对应的是一个函数对象,就是当前正在指向的函数的对象;
function fun(){
	console.log(arguments); //[object Arguments]
	console.log(arguments.length); //3
	console.log(arguments[1]); //3
	console.log(arguments.callee); 
	console.log(arguments.callee == fun); //true
}

fun("Hello", "a", 4);

3.3 数组

  数组(Array),也是一个对象,用来存储一些值,数组是使用数字来作为索引来操作元素。索引(index):以0开始的正整数。

数组的基本操作

  • 创建一个数组对象: var arr = new Array();
  • 创建一个指定长度的数组: var arr = new Array(n);
  • 向数组中添加元素:数组[索引] = 值;
  • 获取数组的长度:数组.length;
  • 修改数组的长度:数组.length = n; 修改比原来的长,多出来的将空出来 修改比原来短,则多出的会被删除;

数组字面量
  使用数组字面量来创建一个数组:

//构造函数创建一个数组
var arr = new Array();

//使用字面量来创建空数组
var arr2 = [];

//使用字面量创建数组时,在创建时就指定数组中的元素;
var arr3 = [1, 2, 3, 4];

//使用构造函数创建对象也可以在创建时赋初值
var arr4 = new Array(1, 3, 4);

//注意这种情况
var arr5 = [10]; //数组中有一个元素,10
var arr6 = new Array(10); //数组长度为10

//数组中的元素的值可以是任意数据类型
var obj = {name: "frank"};
var arr7 = ["Hello", 1, false, obj];
var arr8 = [{name: "frank"},{name: "Alice"},{name: "Tom"}];

//数组中存放函数:
var arr9 = [function(){console.log("这是存放在数组中的第一个函数");},
	function(a,b){console.log("和为:" + (a+b));}];
console.log(arr9);
//调用函数
arr9[1](1,2);

//二维数组
var arr10 = [[1,2],[3,4,5]];
console.log(arr10[1][0]);

遍历数组

  • 方法一:使用for循环
//打印数组stdArr中的每一个元素:
for(var i = 0; i < stdArr.length; i++) {
    console.log(stdArr[i].toString());
}
  • 方法二:使用forEach()方法
    • forEach()方法需要一个函数作为参数
    • 像这种函数,又我们创建但是不需要我们调用的,我们称之为回调函数
    • 数组中有几个元素,函数就会执行几次,每次执行时,浏览器会将遍历到的元素以实参的形式传递进来;我们可以定义形参来获取这些内容;
    • 浏览器会在回调函数中传递三个参数:
      • 第一个:当前正在遍历的元素的值;
      • 第二个:当前正在遍历的元素的索引;
      • 第三个:当前正在遍历的数组;
var arr = ["Hello", "World", "guojingan", 5];
arr.forEach(function(value, index, obj){
			console.log(value);
		});

数组的一些常见方法

  • push()
    • 在数组末尾添加一个或多个元素,返回值是添加元素之后的数组的长度;
  • pop()
    • 删除数组的最后一个元素,返回值是被删除的元素;
  • shift()
    • 删除数组的第一个元素,返回值是被删除的元素;
  • unshift()
    • 向数组开头添加一个或多个元素,并返回添加后数组的长度;
  • slice()
    • 可以从数组中截取指定元素;
    • 不会改变原本的数组,而是将截取的结果封装到一个数组中并返回;
    • 参数:
      • 第一个:截取开始的索引位置,包括该索引;
      • 第二个:截取结束的索引位置,不包括该索引;(第二个参数选填,不填则视为截取第一个参数到数组结束);
      • 注意:索引可以传递一个负值, 最后一个元素索引为-1;
  • splice()
    • 可以删除数组中指定的元素;
    • 会影响原数组,该方法的返回值是被删除的元素;
    • 参数:
      • 第一个: 表示开始位置的索引;
      • 第二个: 表示要删除的个数;
      • 第三个及以后:可以将一些元素插入到开始索引位置的前面;
  • concat()
    • 连接数组,不会改变原数组,返回值是连接后的数组;
  • join()
    • 该方法可以将一个数组转换成一个字符串,返回值是转换后的字符串
    • 在join()默认使用逗号连接,我们可以指定一个字符串作为参数,这个会成为新的连接符;
  • reverse()
    • 反转数组;
    • 该方法会直接修改原数组;
  • sort()
    • 用来对数组进行排序,该方法会改变原数组;
    • 默认按照Unicode编码进行排序;
    • 按照Unicode编码来排序,在对纯数字排序时,有可能得到一个错误的结果;
    • 我们可以自己来指定排序规则,我们可以在sort()中添加一个回调函数,来指定排序规则,回调函数中需要两个形参,浏览器会分别使用数组中的元素作为实参去调用回调函数;
    • 浏览器会根据回调函数的返回值来决定元素的位置:如果返回一个大于0的值,则交换位置;如果返回一个小于或等于0的值,则元素位置不变;
a.sort(function(a, b){
	return b - a; //降序
	//return a - b; //升序
});

推荐阅读