JS中常见的创建对象的方式主要就是new和Object.create两种了,今天小编系统来整理一下。
new
语法:new constructor([arguments])
使用:
var obj = new Object();
根据上面的语法,我们可以看出new实例化对象是通过构造函数实现的。那么new的时候都做了哪些操作?分析如下:
- 创建类(例如:Car)的实例对象(redCar)【实例._ proto__ = 类.prototype】;
- 把类(Car)当作普通函数执行,并且让方法中的this指向创建的实例;
- 分析函数执行的返回值,如果没有返回值或者返回的是原始值类型,最后返回的都是创建的实例,否则以函数自身返回的为主;
Emm,光看上面的描述可能还是不太清楚。那好办吖,动手去实现一个内置new就明白了!
/*
* 实现内置new------按照上面的流程依次来做
* @params
* fn:要操作的类,也就是要创建这个类的实例
* args:要传递给类fn的参数
*/
function myNew(fn,...args){
//1.创建类fn的实例对象[满足:实例.__proto__ = 类.prototype]
let obj = {};
obj.__proto__ = fn.prototype;
//2.把类fn当作普通函数执行,并改变this指向为=>创建的实例对象
let result = fn.call(obj,...args);
//3.分析返回值【无返回值或者返回值是原始值类型=>创建的实例,否则=>以函数返回的为主】
if(result !== null && /^(object|function)$/.test(typeof result)){
return result;
}
return obj;
}
那么上面的方法存在什么问题吗?或者说有哪些地方可以再进一步优化呢?
因为在IE浏览器中,是禁止我们使用_ proto__的,主要目的是防止我们改变原型指向从而导致原型错乱,也可以理解为IE浏览器并没有提供 proto属性。因此我们可以针对这点对以上代码的步骤1做个优化处理。
优化方案:
function myNew(fn,...args){
//1.创建类fn的实例对象[满足:实例.__proto__ = 类.prototype]
let obj = Object.create(fn.prototype);
//2.把类fn当作普通函数执行,并改变this指向为=>创建的实例对象
let result = fn.call(obj,...args);
//3.分析返回值【无返回值或者返回值是原始值类型=>创建的实例,否则=>以函数返回的为主】
if(result !== null && /^(object|function)$/.test(typeof result)){
return result;
}
return obj;
}
细心的童鞋就可以发现优化处理是用Object.create来创建类的实例对象的,那么关于Object.create,你又了解多少?
Object.create
语法:Object.create([Object])
该方法用来创建一个新对象,使用现有对象来提供新创建的对象的_ proto___ ,因此Object需要是一个对象,然后我们创建一个空对象emptyObj,并且把[Object]对象作为新空对象的原型链指向。即:emptyObj.proto___ = [Object]
特殊:
Object.create(null):创建一个没有原型/原型链的空对象,不是任何类的实例;
但是呢,Object.create()兼容性不太好,那么想要通过这种方式来创建对象该怎么处理呢?话不多说,还是动手去实现一个内置Object.create方法吧!保你拿着它随便用,不用再考虑兼容性问题了!!
由于Object.create可以处理null,其实算作是一个bug,因为typeof null 的结果也是object,因此我们在实现的时候可以把这点考虑进去。
/*
* 实现内置Object.create
* @params
* prototype:新创建对象的原型对象
*/
Object.create = function create(prototype){
//1.对参数类型判断,保证是一个对象
if(prototype == null || typeof prototype != 'object'){
throw new TypeError('Object prototype may only be an Object');
}
//2.此处借助类来实现,创建一个类,让该类的prototype等于传递的原型对象,最后返回这个类的实例
function Temp(){}
Temp.prototype = prototype;
return new Temp();
}
在这个过程中会不会有小伙伴想提个问题,上面的步骤2为什么不直接这么处理:
let obj = {};
obj.__proto__ = prototype;
return obj;
Emm,这样不就又绕回到我们最初的问题了吗?Object.create解决的就是不能使用__ proto __ 的问题,这么写被人掐死的吧,哈哈!所以,请保持清醒哟~~
关于new和Object.create的区别
下面我们通过一个实例来对比它们的区别:
function Car(){
this.color = 'red';
}
let carObj1 = new Car();
console.log(carObj1);
let carObj2 = Object.create(Car);
console.log(carObj2);
new:生成对象的_ proto__ 指向了构造函数的原型对象;
Object.create:生成对象的_ proto__指向传入的参数Car;
同时可以观察出new保留了原构造函数属性,而Object.create丢失构造函数属性。
Object.freeze
概念:该方法用来冻结一个对象。一个被冻结的对象不能再被修改,不能再添加/删除属性,也不能修改该对象已有属性的可枚举性、可配置性、可写性,也不能修改已有属性的值,原型也不能被修改。所以该方法最后返回一个和传入参数相同的对象。
语法:Object.freeze(obj);
使用场景:常量一般用const来定义,就不能被修改了;但是当值为引用类型时,还可以操作对象,扩展或者修改对象属性和方法,这个时候就需要用到Objec.freeze函数了。
let obj = {
age:10,
sex:'男'
}
obj.age = 18;
console.log(obj.age); //18
let freezeObj = Object.freeze(obj);
console.log(obj === freezeObj); //true
obj.age = 20;
console.log(obj.age,freezeObj.age); //18 18
那么如何来简单模拟一个freeze方法呢?先来定义一个对象:
let person = {
name:'aa'
}
我们知道Object.defineProperty可以定义对象的属性特性,比如可不可以删除、可不可以修改等。
Object.defineProperty(person, 'name', {
configurable: false, // 表示能否通过delete删除属性,能否修改属性的特性...
enumerable: false, // 表示是否可以枚举。直接在对象上定义的属性,基本默认true
writable: false, // 表示能否修改属性的值。直接在对象上定义的属性,基本默认true
value: '张三' // 表示属性的值。访问属性时从这里读取,修改属性时,也保存在这里。
})
到这里,name属性就成了不能删除、不可枚举、不可修改属性值的属性了;
如果不想让对象被扩展、删除属性,就要用到Object.seal(),该方法用来封闭一个对象,阻止其添加新属性并将现有属性标记为不可配置。最后结合这两个方法来实现一个freeze方法。
function myFreeze(obj){
//1.判断参数是否为Object类型,如果是就封闭对象,循环遍历对象。去掉原型属性,将其writable特性设置为false
if(obj instanceof Object){
Object.seal(obj); //封闭对象
for(let key in obj){
if(obj.hasOwnProperty(key)){
Object.defineProperty(obj,key,{
writable:false //设置只读
})
//如果属性值依然为对象,要通过递归来进行进一步的冻结
myFreeze(obj[key]);
}
}
}
}
最后简单测试一下:
const person = {
name: 'tt',
childs:['a','b']
}
myFreeze(person);
person.age = 30;
console.log(person.age); //undefined
person.childs[0] = 'cc';
console.log(person); //和之前一样
结果如下: