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 | 是否可枚举 |
将对象设置为不可变
- 使用
writable: false
和configurable: false
,可以将对象的某个属性设置为不可变; - 使用
Object.preventExtensions()
可以禁止一个对象添加新属性; - 使用
Object.seal()
密封对象,该对象无法添加新属性,也无法重新配置和删除任何现有属性; - 使用
Object.freeze()
冻结对象,比Object.seal()
多了一个:无法修改属性值。
Getter和Setter
在ES5
中可以使用getter
和setter
部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象上。getter
是一个隐藏函数,会在获取属性值时调用;setter
也是一个隐藏函数,会在设置属性值时调用。
当给一个属性定中存在getter
或setter
,或者两者都存在是,会忽略掉value
和writable
。
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提供了in
和hasOwnProperty
来判断属性是否存在。
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);
打印结果如下:
score
在obj
内部是一个对象,objest.assign()
复制出来的objCopy
的score
与obj.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);