【深入学习JS】08_深入对象

typeof null为object的原因

不同对象在底层都表示为二进制,在JavaScript中二进制前三位都为0的话会被判断为object类型,null的二进制表示全是0,自然前三位也是0,所以执行typeof null会返回object。

字面量调用方式时会自动转为内置对象

常见内置对象有String、Number、Boolean、Object、Function、Array、Date、RegExp和Error

看一下下面这段代码:

var str = 'Hello Wolrd';
typeof str; // string
str instanceof String; // false;

var strObj = new String('Hello Wolrd');
typeof strObj; // string
strObj instanceof String;  // true
Object.prototype.toString.call(strObj);    // [object string]


console.log(str.length);  // 11
console.log(str.charAt(3));  // l

在最后的时候,我们可以直接在基本类型str上访问属性或者方法,是因为引擎会自动把字面量转为String对象

属性描述符

Object.getOwnPropertyDescriptor()

ES5可以通过Object.getOwnPropertyDescriptor()来获得这个对象的某个属性描述符。

var myObject = {
    a: 2
}

Object.getOwnPropertyDescriptor(myObject, 'a');

// {
//    configurable: true
//    enumerable: true
//    value: 2
//    writable: true
//}

Object.defineProperty()

ES5中可以通过Object.defineProperty()来定义一个对象属性的描述符。

var myObject = {};

Object.definedProperty(myObject, 'a', {
    value: 2,
    configurable: true,
    enumerable: true,
    writable: true
})

描述符详解

描述符作用补充
writable决定是否可以修改属性值writable: false;相当于定义了一个空操作setter
configurable只要属性是可配置的,就可以使用definedProperty()configurable:false的情况下,writable可以由true改为false,但是无法从false改为true。还无法delete该属性。
value变量的值
enumerable是否可枚举

将对象设置为不可变

  1. 使用writable: falseconfigurable: false,可以将对象的某个属性设置为不可变;
  2. 使用Object.preventExtensions()可以禁止一个对象添加新属性;
  3. 使用Object.seal()密封对象,该对象无法添加新属性,也无法重新配置和删除任何现有属性;
  4. 使用Object.freeze()冻结对象,比Object.seal()多了一个:无法修改属性值。

Getter和Setter

ES5中可以使用gettersetter部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象上。getter是一个隐藏函数,会在获取属性值时调用;setter也是一个隐藏函数,会在设置属性值时调用。

当给一个属性定中存在gettersetter,或者两者都存在是,会忽略掉valuewritable

var myObject = {
    get a(){
        return this._a_;
    }
    set a(val){
        this._a_ = val * 2;
    }
}

myObject.a = 2;
console.log(myObject.a);  // 4

也可以对单个属性:

var myObject = {
    a: 2
}

Object.defineProperty(myObject, 'b', {
    get: function(){
        return this.a * 2;
    }
})

console.log(myObject.b);  // 4

判断属性是否存在

有时候我们返回一个对象的属性,可能是返回undefined,这个值可能是属性的值为undefined,也可能是这个属性在对象中不存在。JS提供了inhasOwnProperty来判断属性是否存在。

function Person(){}
Person.prototype.name = 1;

var person = new Person();
person.age = 2;

console.log('name' in person);     // true
console.log('age' in person);      // true
console.log('gender' in person);   // false

console.log(person.hasOwnProperty('name'));   // false
console.log(person.hasOwnProperty('age'));    // true
console.log(person.hasOwnProperty('gender'));  // false

person.name = '王花花';
console.log('name' in person);     // true
console.log(person.hasOwnProperty('name'));   // true

delete person.name;
console.log('name' in person);     // true
console.log(person.hasOwnProperty('name'));   // false

由上面的例子我们不难发现:

  • in判断的是对象的所有属性,像你访问一个属性的时候,如果找不到,会沿着原型链去原型上找;
  • hasOwnProperty只会判断对象实例是否具有某个属性。

对象的创建

工厂模式

function createPerson(name){
    var o = new Object();
    o.name = name;
    o.getName = function(){
        return this.name;
    }
    return o;
}

var person = createPerson('王花花');

缺点:对象无法识别,所有实例指向一个原型。

构造函数模式

function Person(name){
    this.name = name;
    this.getName = function(){
        return this.name;
    }
}

var person  = new Person('王花花');

优点:每个实例指向不同的原型;

缺点:每次创建实例,方法都会被创建一次。

原型模式

function Person(){}
Person.prototype.name = '王花花';
Person.prototype.getName = function(){
    return this.name;
}

var person = new Person();

优点:方法不会被重新创建;

缺点:所有的属性和方法都共享,且无法初始化参数。

组合模式

function Person(name){
    this.name = name;
}
Person.prototype = {
    constructor: Person,
    getName: function(){
        return this.name;
    }
}
var person = new Person();

优点:该共享的共享,该私有的私有;

缺点:封装性不够好。

动态原型模式

function Person(name){
    this.name = name;
    if(typeof this.getName !== 'function'){
        Person.prototype.getName = function(){
            return this.name;
        }
    }
}

var person = new Person('王花花');

寄生构造函数模式

寄生构造函数模式就是比工厂模式在创建对象的时候,多使用了一个new。

function Person(name){
    var o = new Object();
    o.name = name;
    o.getName = function(){
        return this.name;
    }
    return o;
}
var person = new Person('王花花');
console.log(person1 instanceof Person) // false
console.log(person1 instanceof Object)  // true

稳妥构造函数模式

function person(name){
    var o = new Object();
    o.sayName = function(){
        console.log(name);
    }
    return o;
}

var person1 = person('王花花');
person1.sayName();            // 王花花
person1.name = 'sugarMei';
person1.sayName();            // 王花花
console.log(person1.name);    // sugarMei
  • 新创建的实例方法不引用this
  • 不使用new操作符来调用构造函数。

稳妥对象最适合在一些安全的环境中(禁止使用this和new)。稳妥构造函数模式也跟工厂模式一样,无法识别对象所属类型。

对象的拷贝

对象的复制分为两种:当B复制A,修改A时,B也发生了改变,那么就是浅拷贝;否则就是深拷贝

前几节曾经讲过,对象的值是存储在堆内存中,然后将指向堆内存地址的指针存放在栈内存中。

浅拷贝就是复制了指针地址,当修改值时,堆内存的值发生改变,所有指向这个内存的指针指向的是同一个内存,所以就会一起发生改变:

在这里插入图片描述

而深拷贝,就是会重新开辟新的堆内存地址,两个指针指向的是两个不同的堆内存,互不影响:

在这里插入图片描述

实现浅拷贝

使用Object.assign
var obj = {
    name: '王花花',
    content: 'Hello Wolrd'
}

var objCopy = Object.assign({}, obj);
objCopy.name = 'sugarMei';
console.log(obj, objCopy);

打印结果:

在这里插入图片描述

你会发现上面这个例子,使用Object.assign()实现对象的复制,虽然修改了objCopy,但是obj,没有发生改变,是不是会觉得Object.assign()是深拷贝,这是错误的

var obj = {
    name: '王花花',
    content: 'Hello Wolrd',
    score: {
        math: 80,
        english: 20
    }
}

var objCopy = Object.assign({}, obj);
objCopy.name = 'sugarMei';
objCopy.score.english = 60;
console.log(obj, objCopy);

打印结果如下:

在这里插入图片描述

scoreobj内部是一个对象,objest.assign()复制出来的objCopyscoreobj.score共享内存,所以object.assign()是浅拷贝

使用扩展运算符
var obj = {
    name: '王花花',
    content: 'Hello Wolrd',
    score: {
        math: 80,
        english: 20
    }
}

var objCopy = { ...obj };
objCopy.name = 'sugarMei';
objCopy.score.english = 60;
console.log(obj, objCopy);

打印结果与Object.assign()一致:

在这里插入图片描述

如果是数组,我们可以利用数组的一些方法比如:slice、concat 返回一个新数组的特性来实现拷贝。

手写浅拷贝

遍历对象,将属性和属性值放在一个新对象中:

var shalldowCopy = function(obj){
    if(typeof obj !== 'object') return;
    let newObj = obj instanceof Array ? [] : {};
    
    // in会访问到原型上的所有属性,但是hasOwnProprty只会访问本实例上的属性
    for(var key in obj){
        if(obj.hasOwnProperty(key)){
            newObj[key] = obj[key];
        }
    }
    
    return newObj;
}

实现深拷贝

使用JSON.parse()和JSON.stringify()
var obj = {
    name: '王花花',
    content: 'Hello Wolrd',
    score: {
        math: 80,
        english: 20
    }
}

var objCopy = JSON.parse(JSON.stringify(obj));
objCopy.name = 'sugarMei';
objCopy.score.english = 60;
console.log(obj, objCopy);

打印结果如下:

在这里插入图片描述

但是不适用于拷贝函数。

手写深拷贝

在拷贝的时候判断一下属性值的类型,如果是对象,就递归调用深拷贝函数:

function deepCopy = function(obj){
    if(typeof obj !== 'object') return;
    let newObj = obj instanceof Array ? [] : {};
    
    for(var key in obj){
        if(obj.hasOwnProperty(key)){
            newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
        }
    }
    
    return newObj;
}

对象的继承

原型链继承

function Parent(){
    this.name = '王花花';
}

Parent.prototype.getName = function(){
    console.log(this.name);
}

function Child(){}

Child.prototype = new Parent();
var child = new Child();
console.log(child.getName());        // 王花花

这样的继承有两个缺点:

  • 引用类型的属性被所有实例共享;
  • 在创建继承实例(child)时,不能给被继承函数(Parent)传参。
function Parent(){
    this.name = '王花花';
    this.score = [60, 50];
}

function Child(){}

Child.prototype = new Parent();
var child1 = new Child();
child1.score.push(100);

var child2 = new Child();
child2.name = '小明';
child2.score.push(10);

console.log(child1.name, child1.score);    // 王花花 [60, 50, 100, 10]
console.log(child2.name, child2.score);    // 小明 [60, 50, 100, 10]

由上述例子可知,首先我们创建实例的时候,无法传值;其次,Child的原型继承Parent,由Child创建的两个实例对象中的对象属性属于共同引用,因此这些对象属性一旦发生改变,两个实例中的这些属性也会一起发生改变。

借用构造函数

function Parent(name){
    this.name = name;
    this.score = [60, 50];
    this.addScore = function(score){
        this.score.push(score);
    }
}

function Child(name){
    Parent.call(this, name);
}

var child1 = new Child('王花花');
child1.addScore(100);

var child2 = new Child('小明');
child2.addScore(10);

console.log(child1.name, child1.score);   // 王花花 [60, 50, 100]
console.log(child2.name, child2.score);   // 小明 [60, 50, 10]

借用构造函数弥补了原型链继承的不足,可以传递参数,也不会共享实例中的引用类型,但是也有缺点:

  • 方法都在构造函数中定义;
  • 每次创建实例都会创建一遍方法。

组合继承

这是原型链继承和构造函数继承的结合。

function Parent(name){
   this.name = name;
   this.score = [60, 50]; 
}

Parent.prototype.addScore = function(score){
    this.score.push(score);
}

function Child(name, age){
    Parent.call(this, name);
    this.age = age;
}

Child.prototype = new Parent();                     // 调用父构造函数1次
Child.prototype.constructor = Child;

var child1 = new Child('王花花', 12);               // 创建实例的时候会再次调用父构造函数(Parent.call(this, name))
child1.addScore(100);

var child2 = new Child('小明', 18);
child2.addScore(10);

console.log(child1.name, child1.age, child1.score);   // 王花花 12 [60, 50, 100]
console.log(child2.name, child2.age, child2.score);   // 小明 18 [60, 50, 10]

这是JS中最常用的继承模式。

缺点就是调用了两次构造函数。

原型式继承

function createObj(o){
    function F(){}
    F.prototype = o;
    return new F();
}

var parent = {
   name : '王花花',
   score : [60, 50]
}

var child1 = createObj(parent);
child1.score.push(100);

var child2 = createObj(parent);
child2.name = '小明';
child2.score.push(10);

console.log(child1.name, child1.score);   // 王花花 [60, 50, 100, 10]
console.log(child2.name, child2.score);   // 小明 [60, 50, 100, 10]

createObj就是 ES5Object.create 的模拟实现,将传入的对象作为创建的对象的原型。

缺点依旧是引用类型属性值会共享。

注意: 修改name值互不影响,是因为这个name是在child2中新增的,而不是修改prototype上的name的值。

寄生组合式继承

寄生式组合继承就是为了弥补组合继承两次调用构造函数的。

回顾一下组合式继承:

function Parent(name){
   this.name = name;
   this.score = [60, 50]; 
}

Parent.prototype.addScore = function(score){
    this.score.push(score);
}

function Child(name, age){
    Parent.call(this, name);
    this.age = age;
}

Child.prototype = new Parent();                     
Child.prototype.constructor = Child;

var child1 = new Child('王花花');

创建child1时,调用了两次构造函数,另外Child.prototype = new Parent();Child会继承Parent中多余的属性和方法,即打印Child.prototype

在这里插入图片描述

打印child1

在这里插入图片描述

在继承的过程中由于调用了Child.prototype = new Parent()Child.prototype还继承了Parent.prototype上的所有属性。正常的操作应该是Child不应该存在这些属性,当child1中查询不到要访问的属性的时候,会向child1.prototype上查找,如果找不到就会向Child.prototype上查找,我们可以使用以下代码来优化:

var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();

最后封装:

function object(o){
    function F(){};
    F.prototype = o;
    return new F();
}

function prototype(child, parent){
    var prototype = object(parent.prototype);
    prototype.constructor = child;
    child.prototype = prototype;
}

prototype(Child, Parent);

function Parent(name){
   this.name = name;
   this.score = [60, 50]; 
}

function Child(name, age){
    Parent.call(this, name);
    this.age = age;
}

var child1 = new Child('王花花', 18);
console.log(child1);
console.log(Child.prototype);

在这里插入图片描述

参考

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值