前端开发系列015-基础篇之Javascript面向对象(四)
一、类型检查对比
在JavaScript代码中经常需要对变量(数据)进行类型的检查,譬如检查某个变量是否是字符串、某个对象是否是数组等,这里简单总结常用了四种进行类型检查的方式。
> ① **typeof**关键字
> ② **constructor**属性
> ③ **instanceof**关键字
> ④ **Object.prototype.toString**方法
typeof 关键字
typeof
是JavaScript中的关键字,使用该关键字可以简单的来判断变量的类型。
var demo; //undefined
var str = "Nice!";
var num = 123;
var bool = true;
var obj = {name:"zs"};
var arr = [1,23,4];
var fn = function(){};
console.log(typeof demo); //"undefined"
console.log(typeof str); //"string"
console.log(typeof bool); //"boolean"
console.log(typeof obj); //"object"
console.log(typeof arr); //"object"
console.log(typeof fn); //"function"
console.log(typeof null); //"object"
typeof
关键字在使用的时候,语法形式为typeof 变量(数据)
,返回值为字符串类型的数据。使用时该关键字能够简单的进行数据类型区分,但存在一些局限性(譬如数组类型得到的结果为object
)。
constructor 属性
constructor
属性主要用来判断对象的类型,更明确的说是用来检查某个对象的构造函数。
function Person(){}
function Student(){}
var p = new Person();
var s = new Student();
console.log(p.constructor); //Person
console.log(s.constructor); //Student
说明 constructor
是实例对象对应原型对象上面的属性,构造函数和原型对象相互关联,构造函数通过prototype
属性来访问原型对象,原型对象通过constructor
属性来找到对应的构造函数。当使用实例对象来访问构造器属性的时候,使用的其实是原型对象的属性。即上面代码中p.constructor 其实是p.__proto__.constructor
。
instanceof 关键字
instanceof
关键字的用于检查某个构造函数的原型对象是否在当前实例对象的原型链中。大部分(构造函数的原型对象没有被替换覆盖)情况下,可以简单认为instanceof
用来检查实例对象和构造函数的实例化关系,返回布尔类型的值。
function Perosn(){}
function Student(){}
var o = new Object();
var p = new Perosn();
console.log(o instanceof Object); //true
console.log(p instanceof Perosn); //true
console.log(p instanceof Student); //false
console.log(Object instanceof Function) //true
console.log(Function instanceof Object) //true
console.log(Function instanceof Function); //true
Object.prototype.toString`方法
Object.prototype.toString
方法是用来进行类型检查的最强有力的武器,该方法作为Object
的原型方法实现,返回一个类似[object Object]
结构的字符串。方法返回值本身是字符串类型的,主要包含对象类型和构造函数两部分内容。
var obj = {};
var arr = [1,2,3];
console.log(arr.toString());//“1,2,3”
console.log(Object.prototype.toString.call(arr)); //“[object Array]”
console.log(obj.toString());//“[object Object]”
console.log((10).toString()); //“10”
console.log((10).toString(2)); //"1010"
console.log(Object.prototype.toString.call(10)); //[object Number]
//数组类型的判断(兼容性处理)
function isArray(array){
if(Array.isArray)
{
return Array.isArray(array);
}else
{
return Object.prototype.toString.call(array) == "[object Array]";
}
}
console.log(isArray(arr)); //true
二、对象的创建和拷贝
这里先简单总结下创建对象的几种方式,然后对浅拷贝和深拷贝进行说明。
//创建对象的几种方式总结
[1] 对象字面量方式创建 var o = {};
[2] 内置构造函数创建 var o = new Object();
[3] 简单工厂函数创建
[4] 自定义构造函数创建 function Person(){} var p = new Person();
[5] Object.create函数创建 var o = Object({Person.prototype});
[6] Object.assgin函数创建(合并)
//备注和说明
Object.create函数在创建对象的时候,会把参数设置为新对象的原型对象(ES5)。
Object.assgin函数主要用来合并对象(ES6)。
浅拷贝就是在执行对象拷贝操作的时候只拷贝一层,也因此如果目标对象的成员值是引用类型的数据,那么就会存在数据共享的问题,浅拷贝可以简单的通过循环赋值
来实现。
var obj1 = {
age:18,
name:"wendingding",
friends:["LiuY","Xia"],
car:{
type:"A",
typeName:"奥迪",
number:"粤A66666"
}
}
var obj2 = {};
function copy(obj1,obj2)
{
for(var key in obj1)
{
obj2[key] = obj1[key];
}
}
copy(obj1,obj2);
console.log(obj2); //{age: 18, name: "wendingding", friends: Array(2), car: {…}}
obj2.friends.push("Hui");
//引用类型的成员存在共享问题
console.log(obj1.friends); //["LiuY", "Xia", "Hui"]
console.log(obj2.friends); //["LiuY", "Xia", "Hui"]
深拷贝
深拷贝就是在执行对象拷贝操作的时候完全(彻底)拷贝
,当调用方法对两个对象进行深拷贝操作完成之后,维持两份数据的副本(两个对象间没有任何的关系)。
var obj1 = {
age:18,
name:"wendingding",
friends:["LiuY","Xia"],
car:{
type:"A",
typeName:"奥迪",
number:"粤A66666"
}
}
var obj2 = {};
function deepCopy(target,obj)
{
obj = obj || {};
for(var key in target)
{
//检查当前成员是否为对象的实例成员
if(target.hasOwnProperty(key))
{
if(typeof target[key] == "object")
{
obj[key] = Array.isArray(target[key]) ? [] : {};
deepCopy(target[key],obj[key]);
}else
{
obj[key] = target[key];
}
}
}
}
deepCopy(obj1,obj2);
obj2.friends.push("Xiao");
console.log(obj1.friends); // ["LiuY", "Xia"]
console.log(obj2.friends); // ["LiuY", "Xia", "Xiao"]
console.log(obj1.car == obj2.car); //false
三、作用域安全的构造函数
构造函数本身是一个函数,在调用的时候有多种调用方式。
> ① `new 构造函数()`调用
> ② `构造函数()`调用(普通函数)
因为构造函数本质上和普通函数没有任何区别,所以完全可以像普通函数一样的直接来进行调用,而如果作为普通函数来进行调用,那么函数内部的this会指向当前的上下文环境(window
)并不安全。这里尝试给出作用域安全的构造函数,并简单介绍其实现的方式。
function Person(name) {
if (this instanceof Person)
{
//设置实例属性和方法
this.name = name;
this.showName = function () {
console.log(this.name);
}
}else
{
return new Person(name);
}
}
//02 创建对象
var p1 = new Person("lisi");
var p2 = Person("wendingding");
p1.showName(); //lisi
p2.showName(); //wendingding
new是从构造函数生成实例对象的命令。ES6为new命令引入了target属性。在构造函数中new.target指向的当前的构造函数,如果构造函数不是通过new调用的,那么该属性值为undefined。我们可以通过该属性来确定构造函数的调用方式。
function Person(name) {
if (new.target === Person) //if (new.target != undefined)
{
this.name = name;
this.showName = function () {
console.log(this.name);
}
}else
{
return new Person(name);
}
}
var p1 = new Person("lisi");
var p2 = Person("wendingding--");
p1.showName(); //lisi
p2.showName(); //wendingding--
四、函数的调用方式和This的丢失
**函数的几种调用方式**
> 01 **普通函数调用** `this指向全局对象window`
> 02 **构造函数调用** `this指向新创建的对象`
> 03 **对象的方法调用**this指向调用的对象本身`
> 04 **通过call 或者是apply方式调用(函数上下文)** `this指向的是当前的上下文对象`
var obj = {
name:"张三",
getName:function () {
console.log(this.name);
}
};
//以对象的方法来进行调用
obj.getName(); //张三
var getName = obj.getName;
getName(); //window.name 为空值
说明 obj.getName()
是以对象方法进行调用,函数内部的this指向的当前的对象(obj),而getName()
表示以普通函数方式调用,this指向的是window
。
//01 获取页面中id值为demo的标签
// var div = document.getElementById('demo');
// var getId = document.getElementById;
// var div = getId('demo');//会报错?
// console.log(div);
//借用apply来修正this
document.getElementById = (function (func) {
return function () {
return func.apply(document,arguments);
}
})(document.getElementById);
var getId = document.getElementById;
var div = getId('demo');//会报错?
console.log(div);
代码说明
01 因为document.getElementById方法的内部实现中需要使用到this,这个this本来期望指向的是document对象
02 当我们以document.getElementById来调用的时候,内部的this指向document对象
03 但是当我们以getId的方式调用的时候,内部的this指向的是window对象(因为我们以普通的方式进行调用)
五、函数的特殊之处
函数的特殊之处在于,它本身是对象,且函数可以提供作用域。
(01) 函数可以在运行时动态的创建,还可以在程序执行过程中创建。
(02) 函数可以赋值给变量,可以被扩展,甚至是删除。
(03) 函数可以作为其他函数的参数和返回值。
(04) 函数可以拥有自己的属性和方法。
函数是第一型对象
01 函数可以像普通对象一样作为函数的参数
02 函数可以像普通对象一样赋值给变量(函数表达式)
03 函数可以像普通对象一个作为函数的返回值返回
04 函数是可以通过()调用并执行的对象。
//函数作为参数传递
setTimeout(function () {
console.log(1);
},100);
//函数作为返回值
function func() {
return function () {
console.log("demo");
}
}
var f = func();
f();//demo
//函数赋值给变量
var a = function(){}
a(); //直接通过变量的名称调用函数
函数的特征 是可以调用的特殊对象 && 可以创建作用域
函数的创建
001 使用字面量的方式来创建函数(或者是函数字面量)
var add = function add(a,b){return a + b;}; //命名函数表达式
var add = function (a,b){return a + b;}; //函数表达式,匿名函数
function add(){}; //函数声明
//01 函数声明
function demo01() {}
//02 函数表达式(匿名函数)
var demo02 = function () {};
//03 命名函数表达式
var demo03 = function demo03Test() {};
//04 打印name属性
console.log(demo01.name); //demo01
console.log(demo02.name); //demo02 (注意在火狐浏览器中打印出来的name属性为空)
console.log(demo03.name); //demo03Test
函数回调
回调函数(回调) 当我们把某个函数作为参数传递给另一个函数的时候,这个函数就称为回调函数
函数回调的基本模式
function func(callBack) {
//处理其他的操作
callBack(); //调用回调函数
}
function demo() {
console.log("这是一个回调函数");
}
func(demo); //注意调用函数的时候,参数是回调函数的引用(不要加括号);
函数回调中的this
如果回调函数是某个对象的方法,而该对象方法中使用了this指针那么该方法作为回调函数来使用的时候,那么需要注意this的指向。
//01 提供一个对象,该对象中永远showName方法
var obj = {
name:"默认的名字",
age:30,
showName:function () {
console.log(this.name);
},
showAge:function () {
console.log(this.age);
}
};
//02 提供一个函数,该函数接受一个参数(函数引用)
function demo(callBack,callBack_obj) {
//处理第一个参数传递对象方法字符串的形式
if(typeof callBack == 'string')
{
callBack = callBack_obj[callBack];
}
if (typeof callBack == 'function')
{
callBack.call(callBack_obj);
}
}
//demo(obj.showName,obj);
//demo(obj.showAge,obj);
//传递字符串和对象来进行调用
demo("showName",obj);
代码说明
(01)以上代码传入两个参数,分别为具体的回调函数,和该回调函数所属的对象
(02)该函数的参数接受两种方式的回调传递(一种是直接传递函数引用,一种是传递对象方法的字符串)
(03)在函数内部对传入的回调参数做处理,修正this的问题
函数作为函数的返回值
① 使用闭包实现一个计数器(setup函数的返回值为一个函数)
② 通过调用返回值(一个函数),可以操作setup函数中的变量
var setup = function () {
var count = 0;
return function () {
return count ++;
}
}
var next = setup();
console.log(next()); //0
console.log(next()); //1
console.log(next()); //2
惰性函数定义
定义 某个函数直到第一次使用的时候才被正确的定义,并且其具有向后惰性执行更少的工作。
场景 函数有一些初始化的准备工作要做,且只需要执行一次的情况。
特点 能够更新自己(函数)的实现。
问题
01 当重新定义自身的时候,已经添加到原始函数的任何属性都会丢失。
02 如何函数被赋值给了其他的变量或者是对象方法,那么在使用变量或者是对象方法调用时仍然会执行旧的函数体。
function foo() {
console.log("foo!");
foo = function () {
console.log("new foo!");
}
}
//函数的调用
//foo(); //foo!
//foo(); //new foo!
//主要问题
//① 添加属性
//② 把函数赋值给新的变量
//③ 以对象的方法调用函数
备注 当惰性函数定义在处理以上三种情况的时候,使用新的变量名调用或者是是以对象的方法来调用函数,那么该函数在执行的时候并不会更新自身,而是执行旧的函数体的内容。
//01 声明函数foo
function foo() {
console.log("foo!");
foo = function () {
console.log("foo! foo!");
}
}
//02 为foo函数对象添加属性
foo.description = "foo函数的描述信息";
//03 把foo函数赋值给其他的变量
var func = foo;
//04 把foo函数赋值给对象中的方法
var obj = {
showFoo:foo
}
//05 验证并演示输出
func(); //foo!
func(); //foo!
console.log(func.description); //foo函数的描述信息
//总结:01 如果把函数赋值给其他的变量,那么在以其他变量的方式调用时不会更新自身,还是执行旧的函数体
obj.showFoo(); //foo!
obj.showFoo(); //foo!
console.log(obj.showFoo.description); //foo函数的描述信息
//总结:02 如果把函数赋值给对象的方法,那么在以对象方法形式调用时不会更新自身,还是会执行旧的函数体。
foo(); //已经更新过foo函数 foo! foo!
foo(); //已经更新过foo函数 foo! foo!
console.log(foo.description); //undefined
即时函数(IIFE)
定义 在函数声明之后立即执行该函数,全称 Immediately-Invoked Function Expression (IIFE)。
组成
① 使用函数表达式来定义函数(匿名函数)
② 在函数表达式末尾添加一组(),表示立即执行当前函数。
③ 将整个函数包装在()中,具体可以有两种包含方式。
优点
01 不会产生全局变量,在即时函数内部定义的所有变量都是该函数的局部变量,不会造成全局变量污染问题。
02 具有更好的封装性,部无法访问到该函数内部的数据。
//第一种写法
(function () {
console.log("即时函数的第一种写法");
}());
//第二种写法
(function () {
console.log("即时函数的第二种写法");
})();
//备注:其它可用的写法
(function (a) {
console.log(a);
}(20));
+function (b) {
console.log(b);
}(30);
-function (b) {
console.log(b);
}(40);
IIFE的传参和返回值
//01 接受参数
(function (str) {
console.log(str); //hello
})("hello");
//02 提供返回值并赋值给新的变量
var foo = (function () {
return 2 + 1;
})();
console.log(foo); //3
六、图书管理综合案例
这里我们介绍多种代码的组织方式以解决实际问题并简单对比。
实现方式(一) :使用面向过程的方式处理
var bookList = [
{name:"什么是批判",author:"福柯"},
{name:"飞鸟集",author:"泰戈尔"}
];
//增加操作
bookList.push({name:"城堡",author:"卡夫卡"});
//查询操作
for (var i = 0; i < bookList.length; i++) {
var obj = bookList[i];
if (obj.name == "飞鸟集")
{
console.log(obj);
break;
}
}
//修改操作
for (var i = 0; i < bookList.length; i++) {
var obj = bookList[i];
if (obj.name == "飞鸟集")
{
obj.author = "泰戈尔-戈尔泰"
}
}
//删除操作
for (var i = 0; i < bookList.length; i++) {
var obj = bookList[i];
if (obj.name == "飞鸟集")
{
//删除该对象
bookList.splice(i, 1);
}
}
console.log(bookList);
实现方式(二):使用函数来封装图书管理操作
var bookList = [
{name:"什么是批判",author:"福柯"},
{name:"飞鸟集",author:"泰戈尔"}
];
//增加操作
function addBook(bookObj) {
bookList.push(bookObj);
}
addBook({name:"城堡",author:"卡夫卡"});
//查询操作
function getBook(name) {
for (var i = 0; i < bookList.length; i++) {
var obj = bookList[i];
if (obj.name == name)
{
return obj;
}
}
return null;
}
console.log(getBook("什么是批判"));
//修改操作
function updateBookData(name,authorName){
for (var i = 0; i < bookList.length; i++) {
var obj = bookList[i];
if (obj.name == name)
{
obj.author = authorName;
}
}
}
updateBookData("飞鸟集","泰戈尔斯坦");
//删除操作
function removeBook(name) {
for (var i = 0; i < bookList.length; i++) {
var obj = bookList[i];
if (obj.name == name)
{
//删除该对象
bookList.splice(i, 1);
}
}
}
console.log(bookList);
实现方式(三):函数封装-复用形式
var bookList = [
{name:"什么是批判",author:"福柯"},
{name:"飞鸟集",author:"泰戈尔"}
];
//增加操作
function addBook(bookObj) {
bookList.push(bookObj);
}
addBook({name:"城堡",author:"卡夫卡"});
//查询操作
function getBook(name) {
for (var i = 0; i < bookList.length; i++) {
var obj = bookList[i];
if (obj.name == name)
{
return obj;
}
}
return null;
}
console.log(getBook("什么是批判"));
//修改操作
function updateBookData(name,authorName){
var obj = getBook(name);
obj.author = authorName;
}
updateBookData("飞鸟集","泰戈尔斯坦");
//删除操作
function removeBook(name) {
var obj = getBook(name);
var index = bookList.indexOf(obj);
bookList.splice(index,1);
}
addBook({name:"东京人",author:"川端康成"});
console.log(bookList);
实现方式(四):面向对象的方式管理图书
var obj = [
{name:"什么是批判",author:"福柯"},
{name:"飞鸟集",author:"泰戈尔"}
];
function BookManager() {
this.bookList = null;
}
BookManager.prototype = {
constructor:BookManager,
init:function (arr) {
this.bookList = arr || [];
},
addBook:function (bookObj) {
this.bookList.push(bookObj)
},
getBook:function (name) {
for (var i = 0; i < this.bookList.length; i++) {
var obj = this.bookList[i];
if (obj.name == name)
{
return obj;
}
}
return null;
},
updateBookData:function (name,authorName){
var obj = this.getBook(name);
obj.author = authorName;
},
removeBook:function (name) {
var obj = this.getBook(name);
var index = this.bookList.indexOf(obj);
this.bookList.splice(index,1);
}
};
//01 创建对象
var YYManager = new BookManager();
YYManager.init(obj);
console.log(YYManager.bookList);
//02 演示添加操作
YYManager.addBook({name:"花田半亩",author:"田维"});
console.log(YYManager.bookList);
//03 演示查询操作
console.log(YYManager.getBook("花田半亩"));
//04 演示更新操作
YYManager.updateBookData("花田半亩","甜田");
console.log(YYManager.getBook("花田半亩"));;
//05 演示删除操作
YYManager.removeBook("什么是批判");
console.log(YYManager.bookList);
//02 创建第二个人的图书管理对象
var wulitaotaoMManager = new BookManager();
wulitaotaoMManager.init([{name:"装逼速成",author:"六扇门"}]);
console.log(wulitaotaoMManager.bookList);
案例总结
01 提供构造函数,在构造函数内部设置实例化属性
02 把常用的操作提取成对象的实例方法,写在原型对象身上
03 提供init方法,用于初始化数据
04 注意:如果切断了原型对象那么需要修正内部的构造器属性指向
七、Class
javaScript语言的传统方法是通过构造函数定义并生成新的对象,这种写法同典型面向对象语言的写法差异很大。ES6引入了Class(类)的概念来作为对象的模板,通过class关键字
来定义类。
类的基本结构
class Person{
constructor(){
this.name = "zs";
this.age = 18;
}
showName(){
console.log("姓名:" + this.name);
}
showAge(){
console.log("年龄:"+ this.age);
}
static test(){
console.log("这是一个测试的方法");
}
}
var p = new Person();
类的重要事实
> ❏ 类和模块的内部默认使用严格模式
> ❏ 区别于ES5,类内定义的所有方法都是不可以被枚举的
> ❏ Class可以看做是语法糖,可以看做是对ES5中构造函数进行了一层封装
> ❏ 事实上,类的所有方法都被定义在类的原型对象(prototype)上,所有的实例共享
> ❏ constructor是类的默认方法,通过new创建对象时总默认调用该方法,若没定义则默认添加