JavaScript -- 对象与原型链 [[Prototype]]

一、第三章:对象

1.属性描述符(数据描述符)

可以使用 Object.getOwnPropertyDescriptor( myObject, "a" ); 获取myObject对象中属性a的属性描述符。

举个例子

var obj = {
    a:1
}
console.log(Object.getOwnPropertyDescriptor(obj,'a'));

使用 Object.defineProperty( myObject, "a", { value: 2, writable: true, configurable: true, enumerable: true } )  添加或修改a属性的属性描述符。

举个例子

var obj = {
    a:1
}
// 修改a的属性描述符
Object.defineProperty(obj,'a',{
    value:2,
    configurable:true,
    writable:false,
    enumerable:false
});
console.log(Object.getOwnPropertyDescriptor(obj,'a'));

属性描述符(数据描述符)有三个可选值

writable(可写的):表示是否可以修改值。如果不能修改,在严格模式下会报错(TypeError)

举个例子

var obj = {}
// 修改a的属性描述符
Object.defineProperty(obj,'a',{
    value:1,
    writable:false
})
console.log(obj.a); // 1
obj.a = 2;
console.log(obj.a); // 1(非严格模式)  TypeError(严格模式下)

configurable(可配置的):如果是true,则可以通过 Object.defineProperty() 修改三个属性描述符。

举个例子

var obj = {};
Object.defineProperty(obj,'a',{
    value:1,
    configurable:false
});
Object.defineProperty(obj,'a',{
    configurable:true
}); // 直接TypeError

注意1:当设置不可配置的时候,可以将 writable 属性描述符由 true 设置为 false。反之不行(false => true)

举个例子

var obj = {
    a:1
};
Object.defineProperty(obj,'a',{
    configurable:false
});
obj.a = 2;
console.log(obj.a); // 2
Object.defineProperty(obj,'a',{
    writable:false
}); // 不会报错,反而设置成功
obj.a = 3;
console.log(obj.a); // 2

注意2:configurable设置为 false 时,delete删除对象的属性也会失效(非严格),严格模式下不允许删除(TypeError)。

举个例子

var obj = {
    a:1
};
console.log(obj.a); // 1
delete obj.a;
console.log(obj.a); // undefined
Object.defineProperty(obj,'a',{
    value:1,
    configurable:false
})
delete obj.a;
console.log(obj.a); // 1

enumerable(可枚举的):如果false,则表示不能够被枚举(for循环遍历)。

举个例子

var obj = {
    a:1
};
console.log(obj.a); // 1
Object.defineProperty(obj,'b',{
    value:2,
    enumerable:false
});
for(const key in obj) {
    console.log(key); // a,没有b
}

2.不变性

如果希望对象的属性是不可改变的,就可以使用下面四种方式来实现。但是这些方法都是浅不变性,即目标对象引用了其他对象(如函数,对象,数组等),这些函数等是可以变化的。

(1) 对象常量

结合 writable:false 和 configurable:false 就可以实现一个常量属性(不可写、不可delete、不可重定义)

var obj = {};
Object.defineProperty(obj,'AAA',{
    value:1,
    writable:false,
    configurable:false
});
console.log(obj.AAA); // 1
obj.AAA = 2; // 不可修改
console.log(obj.AAA); // 1
delete obj.AAA; // 不可删除
console.log(obj.AAA); // 1
Object.defineProperty(obj,'AAA',{
    value:2
}); // TypeError 不可重定义(其中一个为true,都可重定义)

(2) 禁止扩展

禁止对象添加新属性,且保留已有属性,可以修改已有属性值,可以重新配置。(Object.preventExtensions())

var obj = {
    a:1
};
Object.preventExtensions(obj)
console.log(Object.getOwnPropertyDescriptor(obj,'a')); // 三个属性描述符都为 true
obj.b = 2; // 非严格模式静默失败,严格模式 TypeError
console.log(obj.b); // undefined

(3) 密封

禁止添加新属性,禁止重新配置已有属性(writable从true到false除外),可以修改已有属性的值。(Object.seal())

Object.seal()的本质是Object.preventExtensions() + configurable:false

var obj = {
    a:1
};
Object.seal(obj)
console.log(Object.getOwnPropertyDescriptor(obj,'a'));
// writable: true, enumerable: true, configurable: false
obj.b = 2; // 非严格模式静默失败,严格模式 TypeError
console.log(obj.b); // undefined
obj.a = 3; // 可以修改
console.log(obj.a); // 3

(4) 冻结(最高级别)

禁止添加新属性,禁止重新配置已有属性,禁止修改已有属性,只能读取值。(Object.freere())

Object.freeze()的本质是 Object.seal() + writable:false

var obj = {
    a:1
};
Object.seal(obj)
console.log(Object.getOwnPropertyDescriptor(obj,'a'));
// writable: false, enumerable: true, configurable: false
obj.b = 2; // 非严格模式静默失败,严格模式 TypeError
console.log(obj.b); // undefined
obj.a = 3; // 不可以修改
console.log(obj.a); // 1

3.[[Get]]:获取属性值,[[Put]]:给属性赋值

这两个是对象默认的操作,分别用于获取属性值和给属性赋值。

4.Getter与Setter

可以使用 Getter 和 Setter 部分改写单个属性的默认操作([[Get]]、[[Set]])。Getter和Setter是两个隐藏的函数,当获取属性值和给属性赋值时被调用。

var obj = {
    get a(){
        return 1
    }
};
Object.defineProperty(obj,'b',{
    get(){
        return this.a*2;
    }
});
obj.a = 10;
console.log(obj.a); // 1
console.log(obj.b); // 2
var obj = {};
Object.defineProperty(obj,'a',{
    get(){
        return this.key;
    },
    set(val){
        this.key = val * 2;
    }
});
obj.a = 10; // 赋值触发set
console.log(obj.a); // 20 获取值触发get
var obj = {
    get a(){
        return this.key
        },
    set a(val){
        this.key = val * 2;
    }
};
obj.a = 10;
console.log(obj.a); // 20

5.存在性

在不访问属性值的情况下判断这个对象是否具有某个属性。

方法一:key in obj; 会查找原型链。

方法二:obj.hasOwnProperty('key'); 不会查找原型链,检查当前对象中是否存在。

// 假设obj原型上也找不到b
var obj = {
    a:1
};
console.log(a in obj); // true
console.log(b in obj); // false

console.log(obj.hasOwnProperty('a')); // true
console.log(obj.hasOwnProperty('b')); // false

● 枚举

当属性描述符 enumerable 为 false 时,就表示不可枚举,即在循环中不可遍历。

var obj = {
    a:1
}
Object.defineProperty(obj,'b',{
    value:2,
    enumerable:true
})
Object.defineProperty(obj,'c',{
    value:2,
    enumerable:false
})
for(var key in obj){
    console.log(key); // a b
    console.log(obj[key]); // 1 2
}

关于获取对象中 key 的方法

(1) Object.keys(obj):以数组的形式返回对象中可枚举的key(不会查找原型链)

(1) Object.getOwnPropertyNames(obj):以数组的形式返回对象中的key(不会查找原型链,不论是否可枚举)

var obj = {
    a:1
}
Object.defineProperty(obj,'b',{
    value:2,
    enumerable:true
})
Object.defineProperty(obj,'c',{
    value:2,
    enumerable:false
})
console.log(Object.keys(obj)); // [a,b]
console.log(Object.getOwnPropertyNames(obj)); // [a,b,c]

6.遍历

for...in 循环遍历的是对象的 key,和数组的下标,这样有局限性。ES6新增的 for...of 循环可以直接遍历数组的值。

var arr = ['a','b','c'];
for (var i in arr) {
    console.log(i); // 0 1 2
    console.log(arr[i]); // a b c
}
for (var v of arr) {
    console.log(v); // a b c
}

只有实现了迭代器的数据类型(如数组)才可以使用 for...of 循环遍历,js默认实现了迭代器的数据类型有 Array、String、Map、Arguments、Set、Nodelist。这些都可以使用 for...of 遍历。

可以使用 Symbol.iterator 来获取对象的内部属性@@iterator,@@iterator是一个返回迭代器对象的函数,调用它即可获取当前对象的迭代器。

举个例子

var arr = ['a','b','c'];
var a = arr[Symbol.iterator]();
console.log(a);

console.log(a.next()); // {value:'a',done:false}
console.log(a.next()); // {value:'b',done:false}
console.log(a.next()); // {value:'c',done:false}
console.log(a.next()); // {value:undefined,done:true}
console.log(a.next()); // {value:undefined,done:true}

但是普通 Object 并没有默认实现迭代器,官方解释是说怕影响未来的对象类型。我们可以使用自己手写创建迭代器来实现普通对象的 for...of 遍历。

var obj = {
    a:1,b:2
};
// 定义obj普通对象的迭代器
Object.defineProperty(obj,Symbol.iterator,{
    configurable:true,
    writable:false,
    enumerable:false,
    value:function(){
        // 保存当前对象 (obj)
        var that = this;
        // 获取obj的所有可枚举属性,key是数组
        var key = Object.keys(that)
        var index = 0;
        return {
            next:function(){
                return{
                    // 返回值
                    value:that[key[index++]],
                    done:(index > key.length),
                };
            }
        };
    }
})
var o = obj[Symbol.iterator]();
console.log(o.next());
console.log(o.next());
console.log(o.next());
for(var v of obj){
    console.log(v);
}

 二、原型 [[Prototype]]

在JavaScript每个对象中都有这样一个属性 [[Prototype]] ,它的名字叫做原型,表示对象之间的关联关系。当查找对象中的某个属性或方法时,会沿着 [[Prototype]] 进行查找。

1.属性设置和屏蔽

给对象设置属性的详细过程(如给obj对象设置name属性  obj.name = '小媛'

(1) 如果name属性在obj对象中存在,显而易见直接修改值即可。

(2) 如果在obj对象中不存在,就会查找原型链。当查询至原型链尽头仍未找到,则在obj中创建并赋值。

(3) 当在obj中不存在,而obj的原型链中存在,就会有以下三种情况。

● 如果原型链中的 name 属性是可写的(writable:true),则在obj中创建并赋值属性name(相当于屏蔽了UpObj中的name)。

// 设置obj原型链中的对象
var UpObj = { name:'小明' };
// 使obj的原型指向UpObj
var obj = Object.create(UpObj);
console.log(obj.name); // '小明'
obj.name = '小媛';
console.log(obj.name); // '小媛'

● 如果原型链中的 name 属性是不可写的(writeable:false),则默认失败(非严格)或TypeError)(严格模式)。

var UpObj = {};
Object.defineProperty(UpObj,'name',{
    value:'小明',
    writable:false
})
// 使obj的原型指向UpObj
var obj = Object.create(UpObj);
console.log(obj.name); // '小明'
obj.name = '小媛'; // 严格模式下这行代码TypeError错误
console.log(obj.name); // '小明'

如果使用 Object.defineProperty()设置obj的name则会成功。(相当于obj将会屏蔽UpObj中的name)

var UpObj = {};
Object.defineProperty(UpObj,'name',{
    value:'小明',
    writable:false
})
// 使obj的原型指向UpObj
var obj = Object.create(UpObj);
console.log(obj.name); // '小明'
// obj.name = '小媛'; 
Object.defineProperty(obj,'name',{
    value:'小媛'
})
console.log(obj.name); // '小媛'

● 如果原型链中的name是一个 setter,则必定会调用这个 setter 并且不会将 name 属性添加到obj中。

当然使用 Object.defineProperty() 同上。

隐式屏蔽

指在不经意间屏蔽了原型链上的相同属性。

var anotherObject = {
    a:2
};
var myObject = Object.create( anotherObject );
anotherObject.a; // 2
myObject.a; // 2

anotherObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false

myObject.a++; // 隐式屏蔽!
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty( "a" ); // true

myObject.a++ 相当于 myObject.a = myObject.a + 1

2.函数中 new 关键字

所有函数都有一个不可枚举的 prototype 属性,注意这个属性与 [[Prototype]] 不同,虽然他们都是指向某一个对象。函数的 prototype 属性指向的对象中有一个不可枚举的 constructor 属性,它指向函数本身。

function f(){}
console.dir(f);
console.log(f.prototype.constructor === f); // true

当使用 new 关键字调用函数时,会返回一个对象,这个对象的原型链([[Prototype]])中有一个constructor 属性指向被 new 调用的函数。

function f(){}
console.dir(f);
var a = new f();
console.log(a);
console.log(a.constructor === f.prototype.constructor); // true
console.log(a.constructor === f); // true
console.log(f.prototype.constructor === f); // true

把 constructor 指向 f 函数当成 a 对象是由 f 函数“构造”的,看似比较容易理解,其实这是一个误区。 a.constructor 只是通过 [[Prototype]] 默认的委托指向 f 函数,这与构造毫无关系。并且 constructor 属性是不可被信赖的,它很容易被修改。

function f(){}
f.prototype.constructor = 123;
console.dir(f)
var a = new f();
console.log(a);
console.log(a.constructor === f.prototype.constructor); // true
console.log(a.constructor === f); // false
console.log(f.prototype.constructor === f); // false

3.原型继承

JavaScript的原型继承只是通过原型 [[Prototype]] 关联,而不是真正的继承。

function Foo(name) {
    this.name = name;
}
Foo.prototype.myName = function () {
    return this.name;
};
function Bar(name, label) {
    Foo.call(this, name);
    this.label = label;
}
// 我们创建了一个新的 Bar.prototype 对象并关联到 Foo.prototype
Bar.prototype = Object.create(Foo.prototype);
// 注意!现在没有 Bar.prototype.constructor 了
// 如果你需要这个属性的话可能需要手动修复一下它
Bar.prototype.myLabel = function () {
    return this.label;
};
var a = new Bar("a", "obj a");
console.log(a.myName()); // "a"
console.log(a.myLabel()); // "obj a"

Bar.prototype = Object.create(Foo.prototype); 这行代码的含义是创建一个名为 Bar.prototype 的对象(其实就是Bar中的prototype属性指向这个对象),对象的原型 [[Prototype]] 指向 Foo.prototype。这样就可以将 Foo 函数和 Bar 函数的通过 [[Prototype]] 原型链关联起来。使对象 a 可以访问到 myName属性。

由于Bar.prototype 更改了指向,那么原来所指向的对象会被销毁(垃圾回收器回收)。

如何才能在不销毁 Bar.prototype 旧对象的前提下修改指向。使之同样具有 Bar.prototype = Object.create(Foo.prototype); 这行代码的功能?

● 方法一(ES6之前的做法):使用 __proto__直接修改原型 [[Prototype]]

Bar.prototype.__proto__ = Foo.prototype;

缺点:__proto__不是标准并且不能兼容所有浏览器。

● 方法二(ES6新方法):Object.setPrototypeOf(a,b);将a的[[Prototype]]指向b

Object.setPrototypeOf(Bar.prototype , Foo.prototype);

var a = {num:1};
var b = {name:'77'};
Object.setPrototypeOf(a,b);
console.log(a.name); // 77

4.检查一个实例的继承祖先

判断一个实例(JS中的对象)是否是某个函数的继承祖先。即判断两个对象是否通过原型链联系。不严谨可以说:如何判断一个实例是否是某个函数构造的。

a instanceof Foo:只能用于左侧对象与右侧函数,表示 Foo.prototype 是否在 a 对象的原型链上。

缺点:只能判断"父子",不能判断"爷孙"

function Foo(){};
var a = new Foo();
// a是否是Foo的实例?Foo.prototype是否出现在a的原型链上?
console.log(a instanceof Foo); // true

a.isPrototypeOf(b):表示 a 是否出现在 b 的原型链上。既能判断"父子",又能判断"爷孙",能判断整个原型链

function Foo(){};
var a = new Foo();
// Foo是否出现在a的原型链上?
console.log(Foo.isPrototypeOf(a)); // false
// Foo.prototype是否出现在a的原型链上
console.log(Foo.prototype.isPrototypeOf(a)); // true

此方法可以用于直接判断两个对象

var a = {};
var b = Object.create(a);
// 对象a是否在b的原型链上?
console.log(a.isPrototypeOf(b)); // true
console.log(b.isPrototypeOf(a)); // false

可以使用 Object.getPrototypeOf( a );获取a的原型链,与Foo.prototype比较。

缺点:只能判断"父子",不能判断"爷孙"

function Foo(){};
var a = new Foo();
console.log(Object.getPrototypeOf(a) === Foo.prototype) //true
// 只能判断父子,不能判断"爷孙"
console.log(Object.getPrototypeOf(a) === Object.prototype) //false
// 可以判断"爷孙"
console.log(Object.prototype.isPrototypeOf(a)); // true

5.使用 Object.create(null) 创建一个不受原型链影响的对象,可以用来保存数据。

var a = Object.create(null)
console.log(a);

var a = Object.create(null)
a.num = 1;
console.log(a);

 

猜你喜欢

转载自blog.csdn.net/qq_48113035/article/details/123899869