JavaScript高级面向对象
深拷贝和浅拷贝
拷贝:复制一份,指将对象数据复制
在讨论深拷贝和浅拷贝的时候一定保证对象的属性也是引用类型
深拷贝:如果拷贝的时候,将数据的所有引用结构都拷贝一份,那么数据在内存中完全独立就是深拷贝
浅拷贝:如果拷贝的时候,只针对当前的属性进行拷贝,而属性任然是引用类型,那么就是浅拷贝
car = {name:"宝马"}
p = {name:"张三", age:18, car: car};
var pCopy1 = p; //这不是拷贝,没有对**对象p**做任何拷贝行为
//浅拷贝
var pCopy2 = {};
pCopy2.name = p.name;
pCopy2.age = p.age;
pCopy2.car = p.car;
//深拷贝
var pCopy3 = {};
pCopy3.name = p.name;
pCopy3.age = p.age;
pCopy3.car = {};
pCopy3.car.name = car.name;
代码封装
// 浅拷贝
var p = {
name:"张三",
age:18,
car: car
copy:function () {
//创建对象
var temp = {};
// 拷贝属性
for (var k in this) {
temp[k] = this[k];
}
// 返回对象
return temp;
}
};
var p2 = p.copy();
// 深拷贝
var car = {name:"宝马",
deepCopy:function () {
//创建对象
var temp = {};
// 拷贝属性
for (var k in this) {
if (typeof this[k] === 'object') {
temp[k] = this[k].deepCopy();
} else{
temp[k] = this[k];
}
}
// 返回对象
return temp;
}};
var p = {
name:"张三",
age:18,
car: car
deepCopy:function () {
//创建对象
var temp = {};
// 拷贝属性
for (var k in this) {
if (typeof this[k] === 'object') {
temp[k] = this[k].deepCopy();
} else{
temp[k] = this[k];
}
}
// 返回对象
return temp;
}
};
var p2 = p.copy();
// 需要保证所有的对象中都有copy对象,才能实现深拷贝
上面的深拷贝可以改成
var deepCopy = function () {
//创建对象
var temp = {};
// 拷贝属性
for (var k in this) {
if (typeof this[k] === 'object') {
temp[k] = this[k].deepCopy();
} else{
temp[k] = this[k];
}
}
// 返回对象
return temp;
}
var car = {name:"宝马"};
var p = {name:"张三", age:19, car: car};
//让所有对象都有拷贝的方法
car.deepCopy = deepCopy;
p.deepCopy = deepCopy;
var newP = p.deepCopy();
对象的动态特性
1、在js中,一个对象需要属性,就可以利用“对象.属性名 = 值”的方式为其添加属性,只要赋值成功,对象就新增了属性。
2、对象属性的访问形式
- 点语法:o.name
- 关联数组:o[ ‘name’ ] for (var k in o) {temp[k] = o[k];}
3、凡是需要给对象动态添加成员的时候,必须使用关联数组的语法
值类型与引用类型作为参数
1、作为函数的参数没就是将参数的数据拷贝一份传递给函数的定义中的参数
function foo (num) {}
var a = 123;
foo(a);
- 函数在调用的时候,首先需要将参数中的数据拷贝一份,即数字 123 拷贝一份
- 跳转到函数的定义中(函数体),在此之前完成 参数的赋值,即 num = 123
- 正式的进入函数体内,准备执行函数中的每一句话
2、值类型作为函数参数传递的特征,函数内和函数外是两个不同的变量,仅仅是值相等而已
3、引用类型作为函数参数传递的特征,函数内与函数外是两个不同的变量,但是指向同一个对象 - 因此函数内部允许修改函数外部的对象的数据
构造函数的作用
1、构造函数是用来初始化数据
2、在js中给对象添加属性用的,初始化属性值用的
创建对象的过程
1、代码:var p = new Object();
2、首先运算符new创建了一个对象,它类似于{}
,是一个‘没有任何成员’的对象
3、然后调用构造函数 为其初始化成员
- 构造函数在调用的一开始,有一个赋值操作,即this = 刚刚创建出来的对象
- 因此在构造函数中this,表示 刚刚创建出来的对象
4、在构造函数中利用对象的动态特性为对象添加成员
function Hello() {
this.name = '张三',
this.age = 18,
this.salary = 100000
}
var p1 = new Hello();
等价于
function Hello( o ) {
o.name = '张三';
o.age = 18;
o.salary = 100000;
return o;
}
var p1 = Hello( {} );
异常
概念:异常就是程序在运行过程中出现错误
在js中出现异常,浏览器会给出一段提示,由错误类型和错误信息构成
异常最大特点:一旦出现异常,后面的代码将不再执行
如何处理异常
常见错误分为两大类:
- 运行环境的多变性
- 语法错误,代码错误(不考虑)
方法:
try-catch-finally 语法
try {
//可能会导致错误的代码
} catch (error) {
console.log( error );
//在错误发生时怎么处理
} finally {
//无论上面怎么样,都会执行这一段代码
}
DOM操作
DOM树
1、所谓的DOM操作,操作的是DOM树,对其进行增删改查的操作
- 绘制DOM树:childNodes,attributes
- 从一个中心元素访问其所有的直系亲属元素
* 访问父元素:parentNode
* 访问上一个兄弟元素:previousSibling
* 访问下一个兄弟元素:nextSibling
* 访问第一个属性元素:attributes[1]
* 访问最后一个子元素:lastChild 或者 childNodes[childNodes.length - 1]
执行的代码容易出现重复的对象
- 传统的构造函数的方法的定义会影响性能,容易造成多个对象有多个方法副本,应该将方法单独抽取出来,让所有对象共享该方法。
function Person () {
this.name = "张三";
this.age = 18;
this.gender = "female";
this.say = function () { console.log( "hello" ) };
}
var P1 = new Person();
var P2 = new Person();
console.log( P1.say == P2.say ); // false
改成
function sayHello() { console.log( "hello" ) }
function Person2 () {
this.name = "张三";
this.age = 18;
this.gender = "female";
this.say = sayHello;
}
var P2 = new Person2();
var P3 = new Person2();
console.log( P2.say == P3.say ); //true
- 可以将方法全部放到外面,但是会存在安全隐患
1、在开发中会引入各种框架和库,而下面代码会共享上面的代码,自定义成员越多,出现命名冲突的几率越大(引入框架危险)
2、可能在开发中会有多个构造函数,每一个构造函数应该有多个方法,那么会变的不容易维护(代码繁冗不好维护) - 任意一个对象都会默认的链接到它的原型中
1、创建一个函数,会附带的创建一个特殊的对象,该对象使用 函数.prototype 引用,称其为函数的原型属性
2、每一个由该函数作为构造函数创建的对象,都会默认连接到该对象上。即当在函数中找不到对应属性,会去该对象里找。 - 由构造函数创建出来的众多对象共享一个对象,就是 构造函数.prototype
- 只需要将共享的内容,重复会多占用内存的放到 构造函数.prototype中,那么虽有的对象就可以共享了。
Person3.prototype.say = function() { console.log( "hello" ) }
function Person3 () {
this.name = "张三";
this.age = 18;
this.gender = "female";
}
var P4 = new Person3();
var P5 = new Person3();
console.log( P4.say == P5.say ); //true
常见错误
1、写 构造函数.prototype 的时候,将属性也加到里面,导致该属性再也无法修改
Person4.prototype.name = "张三";
function Person4 () { }
var P6 = new Person4();
console.log( P6.name );
2、赋值的错误
Person5.prototype.name = "张三";
function Person5 () { }
var P8 = new Person5();
var P7 = new Person5();
P8.name = "李四";
console.log( P8.name ); //李四
console.log( P7.name ); //张三
如果是访问数据,当前对象中如果没有该数据就到构造函数的原型属性中去找
如果是写数据,当前对象中有该数据的时候,就是修改值;如果对象没有该数据,那就是添加值
原型的概念
function Person () {}
var P = new Person();
// 1、执行new创建对象
// 2、执行构造函数Person初始化对象(给对象添加属性)
// 3、赋值给变量P
// 现在P 表示的对象 默认连接到 Person.prototype
Person.prototype.good = function () { alert( "好" ); };
P.good();
面对实例的是原型对象
面对函数的是原型属性
P.__proto__ === P.prototype
如何使用原型
1、利用对象的动态特性
- 构造函数.prototype.sayHello = function () {};
2、利用直接替换
Student.prototype = {
sayHello: function () {},
sayGood: function () {}
};
静态成员与实例成员的概念
1、静态成员表示的是 静态方法和静态属性的概念,所谓静态,就是构造函数所提供的,所以是不变的
2、实例成员表示的是实例方法和实例属性,所谓的实例就是由构造函数所创建的对象
一般工具型方法都由静态成员提供,一般与实例对象有关的方法由实例成员表示
绘图实例
function Person () {
this.name = '张三';
this.sayHello = function () {}
}
var p = new Person();
var q = new Person();
function Person () {
this.name = '张三';
}
Person.prototype.sayHello = function () {} \\ 点语法
var p = new Person();
var q = new Person();
function Person () {
this.name = '张三';
}
Person.prototype = {
sayHello: function () {}
}; \\ 替换
var p = new Person();
var q = new Person();
实现一个自定义的集合
function ItcastCollection () {}
// 提供数组的方法为其添加成员
ItcastCollection.prototype = []; // 这个更好
或者
ItcastCollection.prototype = Array.prototype; // 会污染系统内置原型
var arr = new ItcastCollection ();
arr.push( 123 );
arr.push( 456 );
属性搜索原则
- 所谓的属性搜索原则,就是对象在访问属性与方法的时候,首先在当前对象中查找
- 如果当前对象中存在属性或方法,停止查找,直接使用该属性与方法
- 如果对象没有该成员,那么,在其原型对象中查找
- 如果原型对象含有该成员,那么停止查找,直接使用
- 如果原型也没有,就到原型的原型中查找
- 如此往复,直到我们的Object.prototype还没有,那么就返回undefined
- 如果是调用方法报错,该xxxx 不是一个函数
Object.create( 对象 )
作用:
实现继承,创建一个原型继承自参数的对象
var o = {sayHello: function () {}};
var o1 = Object.create( o );
// 此时,o成为o1的原型对象__proto__
问题:如果浏览器不支持Object.create
//不要使用以下这种修改内置对象的方法
if (!Object.create) {
Object.create = function () {};
}
//用这种方式
//该函数要实现原型继承,返回的对象应该继承自obj
function inherit( obj ) {
if ( Object.create ) {
return obj;
} else {
function F() {};
F.prototype = obj;
return new F();
}
}
var arr = inherit( [] );
原型链
凡是对象就有原型,原型又是对象,因此凡是给定一个对象,那么就可以找到它的原型,原型还有原型,如此反复,就构成一个对象序列,称该序列为原型链。
问题:
1、原型链到什么时候终止
2、一个默认的原型链结构式怎么样的
3、原型链结构对已知语法的修正
原型链的结构
凡是使用构造函数,创建处对象,并且没有利用赋值的方式修改原型,就说该对象保留默认的原型链
默认原型链的结构:
function Person() {}
var P = new Person();
// P 具有默认的原型链
// P -> Person.prototype -> Object.prototype -> null
{} 的原型链
{} -> Object.prototype -> null
[] 的原型链
[] -> Array.prototype -> Object.prototype -> null
利用替换方式修改原型后,原型链的结构
function F () {}
F.prototype = [];
var arr = new F();
// arr -> [] -> Array.prototype -> Object.prototype -> null
什么是原型式继承
通过修改原型链结构(增加一个节点,删除一个节点,修改一个节点),使得我们的实例能够使用原型链的所有成员
绘制对象的原型链结构
function Person() {}
var P = new Person();
在js中,所有的对象字面量在解析以后,就是一个具体的对象了,那么可以理解为调用的对应的构造方法
1、例如在代码中写‘{}’,就相当于成‘new Object()’
2、例如在代码中写‘[]’,就相当于成‘new Array()’
3、例如在代码中写‘/./’,就相当于成‘new RegExp( ‘.’ )’
注意:在底层理论中是否确实调用构造函数,不一定,和浏览器版本有关
绘制对象的完整原型链
var o = {
appendTo: function ( dom ) {}
};
function DivTag () {}
DivTag.prototype = o;
var div = new DivTag();
// div -> DivTag.prototype( 就是o ) -> Object.prototype -> null
函数的构造函数就是 Function
在js中使用Function可以实例化函数对象,也就是说在js中函数与不同对象一样,也是一个对象类型,函数是js中的一等公民,好处:
• Function 是使用字符串构建函数,那么就可以在程序运行过程中构建函数
• 以前的函数必须一开始就写好,再经过预解析,一步一步运行
• 假定从服务器那里拿到了“[ 1, 2, 3, 4 ]”
\quad
* 将数组形式的字符串,转换成数组对象
\quad
* var str = “[ 1, 2, 3, 4 ]”;
\quad
* var arr = ( new Function( 'return ’ + str + ‘;’ ) )(); // 自调用函数
问题
1、Function如何使用
2、Function与函数的关系
3、函数的原型链结构
函数是Function的实例
语法:
new Function ( arg0, arg1, …, argN, body );
1、Function中的参数全部是字符串
2、该构造函数的作用就是将参数连接起来组成函数
• 如果参数只有一个,那么表示函数体
• 如果参数有多个,那么最后一个参数表示函数体,前面的所有参数表示函数的参数
• 如果没有参数,表示创建一个空函数
创建一个打印一句话的函数
//传统
function foo () {
console.log( '你好' );
}
// Function
var func = new Function( 'console.log( '你好' );' );
// 功能上,两者等价
foo();
func();
传入函数内一个数字,打印该数字
var func = new Function ("num","console.log(num);");
func(12);
要求函数调用时传入任意个数参数,并且函数返回这些数中的最大数
使用 arguments 后面有讲到
解决代码太长的方法
1、利用 加法 连接字符串
var func = new Function( 'a', 'b',
'console.log( a + b );'
);
func(12, 13);
var func = new Function( 'a', 'b', 'c',
'var res = a > b ? a:b;' +
'var result = res > c ? res:c;' +
'console.log( result );'
);
func(12, 13, 28);
2、ES6的语法(少浏览器实现)
使用键盘左上角的左单引号 表示可换行字符串的界定符
3、(最终)利用DOM特性完成该方法
<body>
<div id="code" style="display: none;">
var res = a > b ? a : b ;
var result = res > c ? res : c ;
console.log( result );
</div>
</body>
<script>
var txt = document.getElementById('code').innerText + '';
//或者
var txt = document.getElementById('code').lastChild.nodeValue;
var func = new Function( 'a', 'b', 'c', txt );
func(12, 13, 28);
</script>
argument 对象
arguments 是一个伪数组对象,它表示在函数调用的过程中传入的所有参数的集合
在函数调用的过程中没有规定参数的个数和类型,因此函数调用具有灵活的特性,那么为了方便实用,在每一个函数调用的过程中,函数代码体内有一个默认对象,它存储着传入参数的所有对象
function foo1 () {
var arg = arguments; // arguments是传入所有的参数的集合
console.log( sum1(arg) );
}
var sum1 = function (arg) {
var result = 0;
for ( var k of arg) {
result = result + k;
}
return result;
};
foo1(1,2,3,4);
js中函数并没有规定如何传参
1、定义函数的时候不写参数,一样可以 在调用时传递参数
2、定义的时候写了参数,调用的时候可以不传参数
3、定义的时候写了一些参数,调用的时候可以随意的传递多个参数
函数的原型链结构
任意一个函数,都相当于Function的实例,类似于{} 与 new Object()的关系
1、函数应该有什么属性 ‘proto’
2、函数的构造函数是 Function
3、函数应该继承自’Function.prototype’
4、‘Function.prototype’ 继承自 ‘Object.prototype’
绘制函数的三角形结构
1、Object函数是Function 的一个实例
2、Object 作为对象,继承自Function.prototype的,又Function.prototype继承自Object.prototype
3、Function 是自己的构造函数
绘制Function的构造原型实例三角形结构
1、在js中任何对象的老祖宗是Object.prototype
2、在js中任何函数的老祖宗是Function.prototype
eval
1、var o = eval( “{ name: ‘jim’, age: 18 }” ); // 报错
这是因为eval将{ } 解释成代码块
2、var o = eval( “{ name: ‘jim’ }” ); // 不报错
// 如果里面有一个name: 数据,那么将这一个解释为标记语言
// 如果想知道表达式的值,就将标记语言去掉 var o = { ‘jim’ };
// 但是如果出现{ name: ‘jim’, age: 18 }
// 那么两个标记是语法错误
注意:所以eval中不用 ‘,’ 和 ‘:’
标记语法
格式
名字:
//其作用是跳出多层循环
Zheli: for ( var i = 0; i < arr.length; i++){
for( var j = 0; j < arr[ i ].length; j++){
if ( true ) {
break Zheli;
}
}
}
绘制完整的原型链结构
• 以Person为例
• P -> Person.prototype -> Object.prototype -> null
• Object构造函数:Object -> Object.prototype -> null
• Person 是Function的实例,继承自Function.prototype
• Object 是Function的实例,继承自Function.prototype
• Function 也是Function创建出来的
• Function.prototype继承自Object.prototype -> null
词法作用域
预解析过程
程序执行过程,会先将代码读取到内存中检查,会将所有的声明在此时进行标记,所谓的标记就是让js解释器知道有这个名字,后面再使用名字的时候,不会出现未定义的错误,这个标记的过程就是 提升
声明:
1、名字的声明,也叫标识符的声明(变量名的声明)
2、函数的声明
- 函数声明包含两部分
- 函数声明与函数表达式有区别,函数声明是单独写在一个结构中,不存在任何语句,逻辑判断等结构中
function foo1() {
function func() {} // 声明
if (true) {
function func2 () {} // 函数表达式
}
var f = function func3 () {}; // 函数表达式
this.sayHelllo = function () {}; // 函数表达式
}
- 首先函数声明告诉解释器有这个名字存在,该阶段与名字声明一样
- 告诉解释器,这个名字对应的函数体是什么
例如
var num = 1;
function num() {
alert( num );
}
num();
分析:
1、预解析代码,提升名字
* 首先,提升名字num
* 再提升函数名,但是名字已经存在,因此,只做第二步,让名字与函数体对应
* 结论就是代码中已经有一个函数num了
2、开始执行代码,第一步从赋值语句开始执行
* 给num赋值为1
* 覆盖了函数
3、调用num,由于num中存储是数字 1 ,因此报错
例如:
var num = 123;
function foo1() {
console.log( num );
var num = 456;
console.log( num );
}
foo1();
1、预解析,提升num和foo1函数
2、执行第一句话:num = 123;
3、执行函数调用
* 函数调用进入函数的一瞬间也需要预解析,这里解析的是变量名num
* 在函数内部是一个独立的空间,允许使用外部的数据,但是现在num声明同名,即覆盖外面的
* 执行第一句 打印num,没有数据,undefined
* 执行第二句,num = 456;
* 执行第三句,打印456
例如:
if (true) {
function f1(){
console.log( 'true' );
}
} else {
function f1(){
console.log( 'false' );
}
}
f1();
这一部分更新
以前是:
1、预解析:提升f1函数,只保留最后提升的内容,所以怎么打印都是false
现在是:
函数存在于结构体重,不会函数提升,只是函数表达式,所以可以打印false和true
问题
function foo () {} // 1
var foo = function () {}; // 2
1、第一句的语法是声明,可以提升,因此在函数定义的上方也可以调用
2、第二句的语法是函数表达式,函数名是foo,它会提升,提升的不是函数体
3、函数表达式也支持名字语法的
var foo = function func() {
}; // 3
func(); // 报错
- 函数有个属性,叫name,表示函数名,只有带有名字的函数定义,才有name属性值,如第二句的name是“”
- 但是,函数表达式的名字,只允许在函数内部使用,IE8可以访问
- () 可以将数据转换为表达式
新的浏览器中,写在if,while,do-while结构中的函数,都会将函数的声明转换成特殊的函数表达式
在js中词法作用域规则
1、函数允许访问函数外的数据
2、整个代码结构中只有函数可以限定作用域
3、作用规则首先使用提升规则分析
4、如果当前作用规则中有名字了,就不考虑外面的名字
var num = 123;
function foo () {
var num = 456;
function foo1 () {
num = 789;
console.log( num ); // 输出 789
}
foo1();
console.log( num ); // 输出 789
}
foo();
console.log( num ); // 输出 123
作用域链
可以发现,只有函数可以制造作用域结构。那么,只要是代码,至少有一个作用域,即全局作用域。凡是代码中有函数,那么这个函数就构成另一个作用域,如果函数中还有函数,那么,在这个作用域中就又可以诞生一个作用。将这样的所有的作用域列出来,可以有一个结构:函数内指向函数外的链式结构
好处:控制词法规则
例如:
function f1 () {
function f2 () {
}
}
var num = 456;
function f3 () {
function f4 () {
}
}
绘制作用域的步骤:
1、看整个全局是一条链,即顶级链,记为0级链
2、看全局作用域中,有什么成员声明,就以方格的形式绘制到0级链上
3、再找函数,只有函数可以限制作用域,因此从函数中引入新链,标记为1级链
4、然后在每一个1级链中再次往复刚才的步骤
变量的访问规则
1、首先看变量在第几条链上,在该链上是否有变量的定义与赋值,如果有,直接使用
2、如果没有,就往上一级找,如果有,直接使用,停止查找
3、如果还没有,再次往上一级查找,直到全局链(0级链),还是没有,就is not defined
4、注意,切记同级链不可混合查找
补充
1、声明变量使用 var ,如果不使用var 声明的变量就是全局变量
2、因为在任何代码结构中都可以使用该语法,那么在代码维护的时候会有问题,所以除非特殊原因,不要这么用
闭包
在js中函数可以构成闭包,一般函数是一个代码结构的封闭结构,即包裹的特性,同时根据作用域规则,只允许函数访问外部的数据,外部无法访问函数内部的数据,即封闭的对外不公开的特性,因此说函数可以构成闭包。
闭包要解决什么问题
1、闭包不允许外界访问
2、要解决的问题是间接访问该数据
例如:
function foo () {
var num = 123; // 用Math.random(),可以更理解一点
return num;
}
var res = foo();
console.log( res );
1、这里确实是访问到函数中的数据了
2、但是该数据不能第二次访问,因为第二次访问的时候又要调用一次foo,表示又有一个新的num = 123出来了
function foo () {
var num = Math.random();
function func() {
return num;
}
return func;
}
或者
function foo() {
var num = Math.random();
return function () {
return num;
};
}
var f= foo();
var res1 = f();
var res2 = f();
console.log( res1 + '\n' + res2 );
0级链无法访问1级链的数据,所以间接的访问0级链操作2级链的函数,来访问1级链的数据
如何获得超过一个数据
function foo () {
var num1 = Math.random();
var num2 = Math.random();
// 使用对象
return {
num1: function () {
return num1;
},
num2: function () {
return num2;
}
}
}
如何读取一个数据和修改这个数据
function foo() {
var num1 = Math.random();
var num2 = Math.random();
return {
get_num1: function () {
return num1;
},
set_num2: function ( value ) {
return value;
}
}
}
var o = foo();
var num1 = o.get_num1();
var num2 = o.set_num2(12);
console.log(num1);
console.log(num2);
基本闭包结构
一般比包的问题就是想办法间接的获取函数内数据的使用权,那么我们可以总结一个基本的使用模型
1、写一个函数,函数内定义一个新函数,返回新函数,用新函数获得函数内的数据
2、写一个函数,函数内定义一个对象,对象中绑定多个函数(方法),返回对象,利用对象的方法访问函数内的数据
闭包的基本用法
闭包是为了实现具有私有访问空间的函数
1、带有私有访问数据的对象
所谓的私有数据,就是说只有函数内部可以访问的数据,或对象内部的方法可以访问的数据
function Person() {
this.name = '张三';
}
// 这里的name可以任意修改,不是私有的
// 1 最简单的实现
function createPerson() {
var __name__ = "";
return {
get_name: function () {
return __name__;
},
set_name: function (value) {
if ( value.charAt( 0 ) === '张' ) {
__name__ = value;
} else {
throw new Error( '姓氏不对,不能改名' );
}
}
};
}
var p = createPerson();
p.set_name( '王二' ); // 报错
2、带有私有数据的函数
var func = (function () {
// 私有数据
return function () {
// 可以使用私有的数据
};
})();
闭包的性能问题
函数执行需要内存,函数中定义的变量会在函数执行后自动回收。凡是因为闭包结构,被引出的数据,如果还有变量引用这些数据的话,那么这些数据就不会被回收。
因此在不使用某些数据的时候,一定要赋值一个null
函数科里化(高阶函数)
定义一个函数,该函数反回一个函数,那么在调用的时候
function foo () {
function func() {
}
return func;
}
foo()()