本文作者:钟昕灵,叩丁狼高级讲师。原创文章,转载请注明出处。
作用域安全的构造函数
构造函数的调用方式存在下面两种:
直接调用:普通函数
使用new一起调用:创建对象
function Person(name, age) {
this.name = name;
this.age = age;
}
console.log(Person("zs", 10));//undefined
console.log(new Person("ls", 12));//初始化了name和age的Person对象
如果我们直接调用Person函数,因为函数默认的返回值为undefined,所以得到undefined结果。
如果使用new关键字来调用Person函数,此时在函数中会默认创建一个对象,并将该对象设置给this,然后将name和age封装到该对象中,最后返回该对象。所以得到的是一个封装好数据的对象。
所以,如果我们想要创建对象,这里必须使用new来调用。但是,在实际来发中,我们有可能会忘记使用new,而是直接调用该构造函数,此时会造成什么问题呢?
-
得不到想要的对象,这是最容易想到的
-
会存在作用域安全的问题,这里需要解释一下
直接调用该函数,那么在函数内部中的this指向window
如果我们在函数中需要修改当前创建对象(this)中的属性时,有可能会不知不觉的将window作用域下的某些变量给修改掉,导致数据错乱的问题。
var name = "今天天气不错";
function Person(name, age) {
this.name = name;
this.age = age;
}
Person("zs", 10);
console.log(name);//zs
此时,在Person函数中的this是指向window的,所以this.name访问到的是函数外面(全局作用域)中的name,并为其赋值为zs,所以,最终得到的name值为zs。
既然存在这样的问题,我们就得解决,那么思路应该是怎样的呢?
首先,造成上面问题的根本原因是程序员在使用的过程中可能会忘记new关键字,而导致作用域不安全的问题
所以,而当没有使用new关键字的时候,构造函数中的this关键字是指向window的
反过来,如果构造函数中的this指向window,说明没有使用new关键字,此时就有了下面的代码:
function Person(name, age) {
if(this == window){
throw "调用构造器需要使用new关键字";
}else{
this.name = name;
this.age = age;
}
}
在构造函数中判断this的指向即可解决忘记new关键字的问题。
但是在ES6中,这种方式存在一定的问题,此时的this不一定是指向window,原因我们后面再说。此时我们换种思路来解决。
如果使用new调用该构造函数,那么this指向的是什么呢?对,是当前构造函数创建的对象,所以根据类型判断也是可以的。
if(!(this instanceof Person)){
throw "调用构造器需要使用new关键字";
}else{
this.name = name;
this.age = age;
}
上面这种方式是完全OK的,下面我们再给出一种方式,大家可以了解一下。
在ES6中,为new引入了一个target属性,如果没有使用new调用构造函数,那么在该构造函数中new.target为undefined,反之为当前的构造函数。
if(!new.target){
throw "调用构造器需要使用new关键字";
}else{
this.name = name;
this.age = age;
}
以上解决了我们在使用构造函数创建对象的过程中可能存在的问题。如果出现了,我们也能够快速的解决。
Function和Object
前面我们学习了Function和Object,而且也学习了instanceof关键字的使用,下面来看几个例子,检验一下大家对前面所学知识点的掌握情况。
function Person() {
}
var p = new Person();
console.log(p instanceof Person);//①
Person.prototype = {
};//修改Person的原型对象
console.log(p instanceof Person);//②
①处的打印结果相信大家都非常清楚,因为p对象是由Person构造函数创建出来的,所以Person构造函数的原型对象在p对象的原型链上,所以使用instanceof判断的结果为true。
②处的结果会受到上面修改Person原型对象的影响,修改之后Person的原型对象不在p的原型链中,所以结果返回false。
var f = new Function();
console.log(f instanceof Function);//①
console.log(f instanceof Object);//②
console.log(Function instanceof Object);//③
console.log(Object instanceof Function);//④
①:返回true
②:返回true
上面两个比较简单,这里就不再做说明了。
③:这个也比较简单,Object的原型对象是所有对象的原型链的终点,所以只要最后是Object,都应该返回true。
④:这个判断稍微有点难度,但是如果大家对于前面画过的原型链的图还熟悉的话,应该能够得到正确答案。
因为Function.prototype是Object这个函数对象的原型对象,所以这句话可以这样说了,Function的原型对象在Object的原型链上,所以该判断理应返回true。
总结:如果大家能比较快的得到上面每个练习的答案的话,说明大家对于instanceof和对象的原型链还是认识的比较透彻了,恭喜大家!
浅拷贝和深拷贝的实现
在开发中,我们会有这样的需求,就是将A对象中的属性或者是方法拷贝到B对象中,而这里的拷贝我们按照拷贝的深度分为浅拷贝和深拷贝,下面我们来分析一下:
var p1 = {
name:"zs",
age:10,
favs:["H5","Java","C"],
wife:{
name:"lily",
age:8
}
}
var p2 = {
};
for(var key in p1){
p2[key] = p1[key];
}
console.log(p2);
上面的代码中,我们将p1对象中的属性拷贝给了p2对象,这种拷贝方式我们称之为浅拷贝,为什么呢?我们来画图说明。
上面是p1对象的内存结构图,通过上面的拷贝操作得到的p2是什么结构呢?
我们得到和0x11一模一样的一份数据,而p2就指向该内存区域的数据,然后在0x44中的favs和wife这两个属性仍然指向0x22和0x33这两块内存区域的数据,所以此时的拷贝只拷贝了对象中的第一层属性,称之为浅拷贝。
浅拷贝在使用的过程中存在数据共享的问题(如果修改p1中的favs或者wife中的数据,p2中的这两个属性也会跟着被修改),因为他们引用的是同一块内存区域的数据。这个问题的我们可以使用深拷贝来实现。
所谓深拷贝,就是将对象引用的对象,或者对象引用的对象的引用的对象,一次往下推,全部都拷贝,大家不共享任何数据。
所以要实现深拷贝,当我们发现属性对应的值是一个对象的时候,应该将该对象拷贝一份,然后赋值给当前属性。
function deepCopy(source,target) {
for(var key in source){
if(source.hasOwnProperty(key)){
//只拷贝当前对象的属性
if(typeof source[key] == "object"){
//如果属性是引用类型的对象
// //根据原属性的类型决定是数组还是普通对象
target[key] = Array.isArray(source[key]) ? [] : {
};
deepCopy(source[key],target[key]);//递归调用,完成所有层次的拷贝
}else{
target[key] = source[key];
}
}
}
}
deepCopy(p1,p2);
通过上面的深度拷贝得到的p2对象是和p1完全不同的两份数据,此时不再存在数据共享的问题。
函数的调用和this的丢失
调用函数大家都非常熟悉了,这里再统一的复习总结一下,需要强调的是,使用不同的方式调用函数,函数内部的this指向存在不同
- 普通调用 fun() this指向调用函数的对象—window
- 对象调用 obj.fun() this指向调用函数的对象—obj
- 使用new关键字调用 new Fun() this指向函数内部创建的新对象
- call或者apply调用 this指向call或者apply方法的第一个参数
所以我们在调用函数的过程中需要时刻关注我们调用方式的不同对this的影响,如下面的案例中就发生了this的丢失问题。
<div