# JS拾遗

# Object.create详解

# Object.create是什么

Object.create的作用是使用指定的原型和属性来创建一个对象。有两个形参:

  • 指定的原型proto
  • 一个可选参数descriptors,属性描述符
Object.create(null) // {}
Object.create({x: 0}, {
    y: {value: 1, enumerable: true}
}) // 相当于Object.defineProperties(Object.create(proto), descriptors)
1
2
3
4

# Object.create(null){}的区别

Object.create(null)不继承任何原型方法,Object.create用于创建一个新对象,其中第一个参数为这个对象的原型,而由于null没有原型,所以该方法不继承Object.prototype

因此,{}等同于Object.create(Object.prototype)

可以用以下代码模拟Object.create:

Object.prototype.create = function(proto){
    var fn = function(){};
    
    fn.prototype = proto;
    
    return new fn();
}
1
2
3
4
5
6
7

# Object.setPrototypeOf

ES6新增了Object.setPrototypeOf,用于设置一个指定的对象的原型 ( 即内部[[Prototype]]属性)到另一个对象或 null

Object.setPrototypeOf(obj, prototype)

`ployfill`
// 仅适用于Chrome和FireFox,在IE中不工作:
Object.setPrototypeOf = Object.setPrototypeOf || function (obj, proto) {
  obj.__proto__ = proto;
  return obj; 
}
1
2
3
4
5
6

# new操作符模拟实现

# new操作符都做了什么:

  • 创建了一个全新的对象
  • 这个对象会被执行[[Prototype]](也就是__proto__)链接
  • 生成的新对象会绑定到函数调用的this
  • 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上
  • 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象
function _new(ctor, ...arguments){
    // ES6 new.target 是指向构造函数
    _new.target = ctor;
    
    const instance = Object.create(ctor.prototype);
    const ctorReturnResult = ctor.apply(instance, arguments);
    
    const isObject = typeof ctorReturnResult === 'object' && ctorReturnResult !== null
    const isFunction = typeof ctorReturnResult === 'function'
    
    if(isObject || isFunction) return ctorReturnResult
    
    return instance
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# new操作符和Object.create的区别

new操作符将父类的属性和方法全都赋给子类

Object.create只修改原型

# JS继承

# 原型链继承

特点:

  • 父类新增原型方法/原型属性,子类都能访问到
  • 简单,易于实现

缺点:

  • 无法实现多继承
  • 来自原型对象的所有属性被所有实例共享
  • 创建子类实例时,无法向父类构造函数传参
  • 要想为子类新增属性和方法,必须要在inherits之后执行,不能放到构造器中
function _inherits(Child, Parent){
    Child.prototype = new Parent()
}
1
2
3

问题

父类的私有属性中有引用类型的属性,那它被子类继承的时候会作为公有属性,这样子类1操作这个属性的时候,就会影响到子类2。

# 借用构造函数继承

在子类型构造函数中通用call()调用父类型构造函数

function Person(name, age) {
    this.name = name,
    this.age = age,
    this.setName = function () {}
}

Person.prototype.setAge = function () {}
function Student(name, age, price) {
    Person.call(this, name, age)  // 相当于: this.Person(name, age)
    /*this.name = name
    this.age = age*/
    this.price = price
}
var s1 = new Student('Tom', 20, 15000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

特点:

  • 解决了原型链继承中子类实例共享父类引用属性的问题
  • 创建子类实例时,可以向父类传递参数
  • 可以实现多继承(call多个父类对象)

缺点:

  • 实例并不是父类的实例,只是子类的实例
  • 只能继承父类的实例属性和方法,不能继承原型属性和方法
  • 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

# 原型链+借用构造函数的组合继承

通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用。

function Person (name, age) {
  this.name = name,
  this.age = age,
  this.setAge = function () { }
}
Person.prototype.setAge = function () {
}
function Student (name, age, price) {
  Person.call(this, name, age)
  this.price = price
  this.setScore = function () { }
}
Student.prototype = new Person()
Student.prototype.constructor = Student//组合继承也是需要修复构造函数指向的
Student.prototype.sayHello = function () { }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

优点:

  • 可以继承实例属性/方法,也可以继承原型属性/方法
  • 不存在引用属性共享问题
  • 可传参
  • 函数可复用 缺点: 调用了两次父类构造函数,生成了两份实例

# 组合继承优化

function Person (name, age) {
  this.name = name,
  this.age = age,
  this.setAge = function () { }
}
Person.prototype.setAge = function () {}
function Student (name, age, price) {
  Person.call(this, name, age)
  this.price = price
  this.setScore = function () { }
}
Student.prototype = Person.prototype
Student.prototype.sayHello = function () { }
var s1 = new Student('Tom', 20, 15000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

优点:

不会初始化两次实例方法/属性,避免的组合继承的缺点

缺点:

没办法辨别是实例是子类还是父类创造的,子类和父类的构造函数指向是同一个。

# 组合继承优化2

function Person (name, age) {
  this.name = name,
  this.age = age
}
Person.prototype.setAge = function () {}
function Student (name, age, price) {
  Person.call(this, name, age)
  this.price = price
  this.setScore = function () { }
}
Student.prototype = Object.create(Person.prototype)//核心代码
Student.prototype.constructor = Student//核心代码
var s1 = new Student('Tom', 20, 15000)
1
2
3
4
5
6
7
8
9
10
11
12
13

# ES6 extends

组合式继承

function _inherits(Child, Parent){
    // Object.create
    Child.prototype = Object.create(Parent.prototype);
    // __proto__
    // Child.prototype.__proto__ = Parent.prototype;
    Child.prototype.constructor = Child;
    // ES6
    // Object.setPrototypeOf(Child, Parent);
    // __proto__
    Child.__proto__ = Parent;
}
1
2
3
4
5
6
7
8
9
10
11

# nullundfined

nullJavaScript的关键字,用来描述空值。因为typeof null == "object",可将null不严谨的认为成一种特殊的对象,即非对象。这是是一个历史悠久的 bug,就是在JS的最初版本中null的内存存储信息是000开头的,而000开头的会被判断为Object类型。

实际上,nullJavaScript的基本类型之一,表示数字、字符串、对象等是无值的。

undefined用以表示更深层次的空值,即未定义。是变量的一种取值,表明变量未被初始化。若查询对象的属性,数组元素等返回undefined,也说明属性或元素不存在。undefined是预定义的全局变量,但不是JavaScript关键字。在ECMAScript 3中是可读写的,可以被赋予任何值,在ECMAScript 5中是只读的。

null == undefined为真,要严格判断需要用===。另外,nullundefined都不包含任何属性和方法,也没有包装对象。

# 不可变的原始值和可变的对象引用

JavaScript中的原始值(undefinednull、布尔值、数字和字符串)是不可更改的:任何方法都无法更改(或“突变”)一个原始值。字符串中所有的方法都会返回一个新的字符串值。

原始值的比较是值的比较:只有在它们的值相等时它们才相等。比较两个字符串,当且仅当它们的长度相等且每个索引的字符都相等时,JavaScript才认为它们相等。

对象的比较并非值的比较:即使两个对象包含同样的属性及相同的值,它们也是不相等的。同样,各个索引元素完全相等的两个数组也不相等。对象值都是引用(reference),对象的比较是引用的比较:当且仅当引用同一个基对象时才相等。因此,对象又被称为引用类型(referencetype),以此与基本类型做区分。

# JavaScript类型转换

转为布尔值

nullundfined""0-0NaN这6个转为false,其他的都会转为truefalse又和这6个值被称为“假值”(falsy value),其他值称做“真值”(truthy value)。

包装对象也会被转换成true,所以new Boolean(false) == true

转为数字

  • 以数字表示的字符串可以直接转换为数字,也允许在开始和结尾处带有空格,但有任意非空格字符的字符串都会转为NaN
  • 空字符串""会被转换为0
  • null转为0,undefined转为NaN
  • true转换为1,false也会被转换为0
  • 空数组会被转换为0,只有一个数字元素的数组例如[2]或者其他唯一一个可以被会被转成数字的字符串,会被转换成对应的数字。而唯一的布尔值元素数组不会被转换为数字。

通过Number()将字符串转为数字时,只能基于十进制数进行转换,并且不能出现非法的尾随字符。parseInt()parseFloat()都会跳过任意数量的前导空格,尽可能解析更多数值字符,并忽略后面的内容,如果第一个非空格字符是非法的数字直接量,将返回NaN。另外,parseInt()可以接收第二个可选参数,这个参数指定数字转换的基数,合法的取值范围是2~36。

转为对象

如果试图把nullundefined转换为对象会抛出一个类型错误(TypeError)。Object()函数在这种情况下不会抛出异常:仅简单地返回一个新创建的空对象。

转为字符串

Number类的toString()方法可以接收表示转换基数(radix)的可选参数,如果不指定,默认转换成十进制。进制基数范围应该在2~36之间。

数字转字符串需要控制小数点的有效数字位数时,可用以下三个方法。toFixed()任何时候都不会使用指数记数法;toExponential()则将数字转换为指数形式的字符串,其中小数点前只有一位,小数点后的位数则由参数指定;toPrecision()根据指定的有效数字位数将数字转换成字符串。这三个方法都会适当地进行四舍五入或者填充0。

对象转换为原始值

对象到字符串(object-to-string)和对象到数字(object-to-number)的转换是通过调用待转换对象的toString方法或valueOf方法来完成的。

toString

  • 对象默认的toString方法返回一个反映这个对象的字符串"[object Object]"
  • 数组的toString方法将每个数组元素转换为一个字符串,其接受一个参数定义分隔符,默认值是逗号,。具体实现就是由数组中的每个元素的toString返回值经调用join 方法连接(由逗号隔开)组成。
  • 函数toString方法返回函数的JavaScript源代码字符串。
  • 日期的toString方法返回一个可读的(可被JavaScript解析的)日期和时间字符串。
  • RegExp类的toString方法返回表示正则表达式直接量的字符串。

valueOf

valueOf方法主要作用是,如果存在任意原始值,就将其转换为表示它的原始值。对象默认的valueOf方法简单地返回对象本身。数组、函数和正则表达式等与之相同。只有日期类不同,valueOf方法会返回一个内部表示:1970年1月1日以来的毫秒数。

鉴于日期类的特殊性,运算符+、==、!=和比较运算符,需要同类型,所以在需要转化成数字的情况下,会调用日期类的toString方法,需要数字的情况下调用valueOf方法。其他运算符的类型转换都是决定的

1 + new Date() //调用toString
new Date() + 1  //调用toString
+ new Date() //调用valueOf
1
2
3

# 作为属性的变量

当声明一个JavaScript全局变量时,实际上是定义了全局对象的一个属性

命名函数的函数名在该函数内部是一个常量,不能被再次赋值,再次赋值时,严格模式下会报错,非严格模式下会静默失败。

# 严格判断数组

Array.isArray()是ES5的方法,可以用Object.prototype.toString.call(arr) === '[object Array]'来进行polyfill。

# instanceof

instanceof通过判断对象的原型链中是不是能找到类型的 prototypeinstanceof的右侧必须为一个对象,否则会报错

'' instanceof String; // false 检查原型链会找到 undefined
[]  instanceof Array; // true
[]  instanceof Object; //true

var myString  = new String();
var newStr    = new String("String created with constructor");
var myDate    = new Date();
var myObj     = {};
var myNonObj  = Object.create(null);

myDate instanceof Object;   // 返回 true
myNonObj instanceof Object; // 返回 false, 一种创建非 Object 实例的对象的方法
1
2
3
4
5
6
7
8
9
10
11
12

怪异的是:

Function instanceof Object // true
Object instanceof Function // true
1
2

并且instanceof无法跨iframes判断数组:

var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[window.frames.length-1].Array;
var arr = new xArray(1,2,3); // [1,2,3]

// Correctly checking for Array
Array.isArray(arr);  // true
Object.prototype.toString.call(arr); // true
// Considered harmful, because doesn't work though iframes
arr instanceof Array; // false
1
2
3
4
5
6
7
8
9
10

# 存取器属性

对象的属性有数据属性和存取器属性两种:

数据属性,包含的一个数据值的位置,可以对数据值进行读写。包含四个特性:

  • configurable: 表示属性是否可配置,即能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或能否把属性修改为访问器属性,默认为true

  • enumerable: 可枚举性,表示能否通过for-in循环返回属性

  • writable:可写性,表示能否修改属性的值

  • value: 包含该属性的数据值。默认为undefined

存取器属性accessor property,又称访问器属性,不同于数据属性,包含的是一对getset方法。在读取属性时调用get函数,在写入属性时调用set函数,因此getset方法分别代替了数据属性的valuewritable特性

# 函数的length属性与arguments.length

函数的length属性指函数定义时所声明的形参的个数,而arguments.length表示的是函数调用时,实参的个数。

实际调用函数时,传入的参数的个数可以比形参的个数少,也可以比它多。