基础篇:数据类型之引用类型

编程语言都具有内建的数据结构,但各种编程语言的数据结构常有不同之处。

JavaScript 是一种弱类型或者说动态语言。这意味着不用提前声明变量的类型,在程序运行过程中,类型会被自动确定。

这也意味着你可以使用同一个变量保存不同类型的数据:

let foo = 42;
foo     = 'bar';
foo     = true;

基本类型和引用类型

JavaScript 语言中类型集合由原始值和对象组成,一般称原始值为基本类型,称对象为引用类型。

基本类型(7种)

JavaScript 数据的基本类型原本有五种:字符串(String)、数值(Number)、布尔值(Boolean)、undefined、null,ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值,另外还有一种表示任意大的整数的 BigInt 类型(目前是第 4 阶段提案)。

注意: 由原始数据类型创建一个显式包装器对象从 ES6 开始不再被支持。 然而,现有的原始包装器对象,如 new Boolean、new String、new Number,因为遗留原因仍可被创建。

基本数据类型的值是按值访问的,有以下特点:

  • 基本类型的值是不可变的。注意: 基本类型变量的修改,是被赋予一个新值,而不是改变原值,也就是说,新值是用新内存创建的,而不是在原值内存中修改的。
  • 基本类型的比较是值的比较
  • 基本类型的变量是存放在栈内存里的。

引用类型

JavaScript 中的对象是引用类型,包括:Object 类型、Array 类型、Date 类型、RegExp 类型、Function 类型等,以及 ES6 新增的 Map 类型、WeakMap 类型、Set 类型、WeakSet 类型等。

引用类型的值是按引用访问的,有以下特点:

  • 引用类型的值是可变的。
  • 引用类型的比较是引用的比较。
  • 引用类型的变量是存放在栈内存中的一个指针,其值是保存在堆内存中的对象,其指针指向堆内存中的对象。

一个简单示例

let a = 1
const b = a
a = 2
console.log(a) // 2
console.log(b) // 1
let a = {
    num: 1
}
const b = a
a.num = 2
console.log(a.num) // 2
console.log(b.num) // 2

对象(Object)

在计算机科学中, 对象(Object)是指内存中的可以被标识符引用的一块区域。

JavaScript 的设计是一个简单的基于对象的范式。一个对象就是一系列属性的集合,一个属性包含一个名和一个值。一个属性的值可以是函数,这种情况下属性也被称为方法。

在 JavaScript 中,Object 是一种用于存储各种键值集合和更复杂的实体的数据类型,可以被看作是一组属性的集合。用对象字面量语法来定义一个对象时,会自动初始化一组属性。而后,这些属性还可以被添加和移除。属性的值可以是任意类型,包括其它对象。属性使用键(key)来标识,它的键值可以是一个字符串或者符号值(Symbol)。

const b = Symbol('b')
const obj = {
    a: 1,
    [b]: 2
}
console.log(obj.a)  // 1
console.log(obj[b]) // 2

对象拥有两种属性:数据属性和访问器属性。数据属性是键值对,而访问器属性有一个或两个访问器函数(get 和 set)来存取数值。

const obj = {
    a: 1,
    get b() {
        if (this.a > 10) { return 10 }
        return 1
    },
    set b(val) {
        this.a = this.a + val / 10
    }
}
console.log(obj.a) // 1
console.log(obj.b) // 1

obj.b = 100 
console.log(obj.a) // 11
console.log(obj.b) // 10

注意: 在 Javascript 中,大括号可以表示对象,也可以表示表达式。

{ foo: 123 }

上述代码,可以是表示一个包含 foo 属性的对象,也可以是表示一个代码区块,里面有一个标签 foo,指向表达式123。为了避免这种歧义,V8 引擎规定,如果行首是大括号,一律解释为对象。不过,为了避免歧义,最好还是在大括号前加上圆括号。

eval('{foo: 123}')   // 123
eval('({foo: 123})') // {foo: 123}

Object对象

在 JavaScript中,几乎所有的对象都是 Object 类型的实例,它们都会从 Object.prototype 继承属性和方法,虽然大部分属性都会被覆盖或者说被重写了。

Object 可以作为构造函数为给定的参数创建一个对象:

  • 如果给定值是 null 或 undefined,将会创建并返回一个空对象。
  • 如果传进去的是一个基本类型的值,则会构造其包装类型的对象。
  • 如果传进去的是引用类型的值,仍然会返回这个值,该值与源对象相同的引用地址。
  • 当以非构造函数形式被调用时,Object 的行为等同于 new Object()。
new Object()          // {}
new Object(undefined) // {}
new Object(null)      // {}
new Object(1)         // Number {1}
new Object('a')       // String {'a'}
new Object(true)      // Boolean {true}

const a = { a: 1 }
a === Object(a)       // true

Object属性操作

  • 读取属性: 使用点运算符或者方括号运算符。

  • 属性的赋值: 使用点运算符或者方括号运算符。

  • 查看所有属性: 使用 Object.keys 方法查看一个对象本身的所有属性。

  • 删除属性: 使用 delete命令删除对象的属性,删除成功后返回 true。注意: 删除一个不存在的属性不会报错,而且返回 true。

    只有一种情况,delete 命令会返回 false,那就是该属性存在,且不得删除。另外,delete 命令只能删除对象本身的属性,无法删除继承的属性。

  • in 运算符: 用于检查对象是否包含某个属性。注意: in 运算符不能识别哪些属性是对象自身的,哪些属性是继承的。

  • for…in 循环: 用来遍历一个对象的全部属性。注意: *for…in遍历的是对象所有可遍历的属性,会跳过不可遍历的属性,并且它不仅遍历对象自身的属性,还遍历继承的属性。
const obj = {
    a: 1,
    abc: 'abc'
}
console.log(obj.a)           // 1
console.log(obj['a' + 'bc']) // 'abc'
obj.c = 'c'                  // 'c'

delete obj.c        // true
obj.c               // undefined
delete obj.aa       // true, 删除不存在属性
delete obj.toString // true, 无法删除继承属性
obj.toString        // ƒ toString() { [native code] }

'a' in obj        // true
'toString' in obj // true

with 语句

with 语句的作用是操作同一个对象的多个属性时,提供一些书写的方便。

const obj = {
    a: 1,
    abc: 'abc'
}
with (obj) {
    console.log(a)   // 1
    console.log(abc) // 'abc'
}

注意: 如果 with 区块内部有变量的赋值操作,必须是当前对象已经存在的属性,否则会创造一个当前作用域的全局变量。

const obj = {
    a: 1,
    abc: 'abc'
}
with (obj) {
    a = 100
    b = 200
}
console.log(obj) // {a: 100, abc: 'abc'}
console.log(b)   // 200

注意: with 语句存在绑定对象不明确的问题,在一些场景中,无法判断变量到底是全局变量,还是对象的一个属性。这非常不利于代码的除错和模块化,编译器也无法对这段代码进行优化,只能留到运行时判断,这就拖慢了运行速度。因此,建议不要使用 with 语句

with (obj) {
    console.log(v)
}

Object静态方法

is()

判断两个值是否为同一个值,与严格比较运算符(===)的行为基本一致,只有两处不同:一是 +0 不等于 -0,二是 NaN 等于自身。

Object.is(+0, -0)   // false
Object.is(NaN, NaN) // true
assign()

用于将所有可枚举属性的值从一个或多个源对象分配到目标对象,返回目标对象。

使用 assign 需注意:

  • 属性名称是 String 类型和 Symbol 类型的属性都会被拷贝,但不会拷贝源对象的访问器(gettter、setter)属性。??

    const obj1 = {
        a: 1,
        get b() { return 'target'; },
        set b(val) { console.log('target') }
    };
    const obj2 = {
        get b() { return 'origin'; },
        set b(val) { console.log('origin') }
    };
    
    const obj = Object.assign(obj1, obj2);
    console.log(obj); // {"a":1,"b":"target"}
    obj.b = 100       // 'target'
    
  • 只会拷贝源对象自身的并且可枚举的属性到目标对象,继承属性和不可枚举属性是不能拷贝的。

    const obj = { a: 1 }
    Object.defineProperty(obj, "b", { // 可枚举属性
        value: 2,
        enumerable: true
    });
    Object.defineProperty(obj, "c", { // 不可枚举属性
        value: 3,
        enumerable: false
    });
    Object.assign(obj, Object.create({ d: 4 })) // 继承属性
    console.log(JSON.stringify(Object.assign({}, obj))) // {"a":1,"b":2}
    
  • 原始类型会被包装为对象。注意: null 和 undefined 会被忽略;除了 String 类型,其他类型自身没有可枚举属性,即,拷贝不会产生效果。

    const obj = Object.assign({}, 'abc', 10, Symbol("foo"), true, null, undefined);
    console.log(obj); // { "0": "a", "1": "b", "2": "c" }
    
    // 注意:只有字符串的包装对象才可能有自身可枚举属性,以上相当于
    const obj = Object.assign({}, 'abc'); // { "0": "a", "1": "b", "2": "c" }
    
  • 异常会打断后续拷贝,但引发错误之前拷贝的属性会覆盖到目标对象,比如,覆盖一个只读属性。

    const obj = Object.defineProperty({}, "foo", {
        value: 1,
        writable: false // 只读属性
    });
    
    Object.assign(obj, { bar: 2 }, { foo2: 3, foo: 3, foo3: 3 }, { baz: 4 });
    // Uncaught TypeError: Cannot assign to read only property 'foo' of object '#<Object>'
    
    console.log(obj.bar);  // 2
    console.log(obj.foo2); // 3
    console.log(obj.foo);  // 1,拷贝失败,还是原来的值
    console.log(obj.foo3); // undefined,异常打断拷贝任务
    console.log(obj.baz);  // undefined,异常打断拷贝任务
    
  • assign() 是浅拷贝,即,如果源对象的属性是一个对象的引用,仅仅会复制其引用。深入理解JavaScript的浅拷贝和深拷贝

    const obj1 = {
        a: 0,
        b: { c: 0 }
    }
    const obj = Object.assign({}, obj1)
    console.log(JSON.stringify(obj)) // {"a":0,"b":{"c":0}}
    
    obj1.a = 1
    obj1.b.c = 1
    console.log(JSON.stringify(obj)) // {"a":0,"b":{"c":1}}
    
  • 对象的扩展运算符等同于使用 assign() 方法,同样,也可以用来处理数(会把数组视为对象)。

    { ...obj };
    // 相当于
    Object.assign({}, obj);
    
create()

使用指定的原型对象和属性创建一个新对象。

Object.create(proto,[propertiesObject])

proto 是新创建对象的原型对象,如果 proto 不是 null 或非原始包装对象,则抛出一个 TypeError 异常;propertiesObject 是对象的属性类型的对象。

const obj1 = {};
// 相当于
const obj2 = Object.create(Object.prototype);

function Constructor(){}
const obj3 = new Constructor();
// 相当于
const obj4 = Object.create(Constructor.prototype);

const obj5 = Object.create(Object.prototype, {
    a: { // 数据属性
        writable: true,
        configurable: true,
        value: "hello"
    },
    b: { // 访问器属性
        configurable: false,
        get: function () { return 10 },
        set: function (value) {}
    }
});
defineProperty()

给添加或修改对象的一个属性,并可以指定该属性的配置,并返回此对象。

Object.defineProperty(obj, prop, descriptor)

对象目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter 函数和 setter 函数所描述的属性。一个描述符只能是这两者其中之一;不能同时是两者。

这两种描述符都是对象,有以下值:

  • configurable:布尔值,默认值:false。值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。注意: 值为 false,只能修改 value 描述符,以及将 writable 改为 false (不能由 false 改为 true) ,其他描述符不可修改。

    当试图改变不可配置属性(除了 value 和 writable 属性之外)的值时,会抛出TypeError,除非当前值和新值相同。

  • enumerable:布尔值,默认值:false。值为 true 时,该属性才会出现在对象的枚举属性中。

  • value(数据描述符): 属性的值,可以是任何有效的 JavaScript 值,默认值 undefined。

  • writable(数据描述符): 布尔值,默认值:false。值为 true 时,属性的值(value),才能被修改。

  • get(存取描述符): 属性的 getter 函数,当访问该属性时,会调用此函数,返回值会被用作属性的值,默认值:undefined。该函数不传入任何参数,但是会传入 this 对象(由于继承关系,this 并不一定是定义该属性的对象)。

  • set(存取描述符): 属性的 setter 函数,当属性值被修改时,会调用此函数,默认值:undefined。该函数接受一个参数(被赋予的新值),同时,也会传入 this 对象。

如果一个描述符不具有 value、writable、get 和 set 中的任意一个键,那么它将被认为是一个数据描述符。如果一个描述符同时拥有 value 或 writable 和 get 或 set 键,则会产生一个异常。

const obj = {};
Object.defineProperty(obj, "a", {
    value: 1,
    writable: true,
    enumerable: true,
    configurable: true
});
Object.defineProperty(obj, "b", {
    get() { return 2; },
    set(val) { console.log(val) },
    enumerable: true,
    configurable: true
});
console.log(JSON.stringify(obj)) // {"a":1,"b":2}

使用点运算符和 defineProperty() 赋值,数据描述符中的属性默认值是不同的:

const ojb = {};
ojb.a = 1;
// 相当于
Object.defineProperty(o, "a", {
    value: 1,
    writable: true,
    configurable: true,
    enumerable: true
});


Object.defineProperty(ojb, "b", { value: 2 });
// 相当于
Object.defineProperty(ojb, "a", {
    value: 2,
    writable: false,
    configurable: false,
    enumerable: false
});
defineProperties()

与 defineProperty() 类似,区别是可以一次操作对象多个属性。

const obj = {};
Object.defineProperties(obj, {
    a: {
        value: 1,
        writable: true,
        enumerable: true,
        configurable: true
    },
    b: {
        get() { return 2; },
        set(val) { console.log(val) },
        enumerable: true,
        configurable: true
    }
});
console.log(JSON.stringify(obj)) // {"a":1,"b":2}
getOwnPropertyDescriptor()

返回指定对象上一个自有属性对应的属性描述符。自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性。

const obj = { a: 1 };
console.log(JSON.stringify(Object.getOwnPropertyDescriptor(obj, 'a')))
// {"value":1,"writable":true,"enumerable":true,"configurable":true}
getOwnPropertyNames()

返回一个由指定对象的所有自身属性(包括不可枚举属性,但不包括继承属性和 Symbol 值作为名称的属性)的属性名数组。

const b = Symbol('b')
const obj = { a: 1, [b]: 2 };
console.log(JSON.stringify(Object.getOwnPropertyNames(obj))) // ["a"]
getOwnPropertySymbols()

返回一个由指定对象自身的所有属性名是 Symbol 类型的属性名数组。

getPrototypeOf()

返回指定对象的原型(内部[[Prototype]]属性的值)。

setPrototypeOf()

为对象设置一个指定的原型 (一个对象或 null),即修改对象 [[Prototype]] 属性的值。

preventExtensions()

防止对象的任何扩展,也就是永远不能再添加新的属性。

注意: 该方法仅阻止添加自身的属性,但其对象类型的原型依然可以添加新的属性。

isExtensible()

判断一个对象是否是可扩展的,即,是否可以添加新的属性。默认情况下,对象是可扩展,preventExtensions、seal、freeze 方法可以标记一个对象为不可扩展。

seal()

密封一个对象,阻止添加新属性并将所有现有属性标记为不可配置,但不阻止原可写属性的值的修改。

注意: seal() 和 freeze() 的区别,freeze() 冻结的对象中的现有属性值是不可变的,seal() 密封的对象可以改变其现有属性值。

isSealed()

判断一个对象是否被密封。密封对象是指那些不可扩展 的,且所有自身属性都不可配置且因此不可删除(但不一定是不可写)的对象。密封对象一般有三种情况:一是 seal 方法标记的对象,二是 preventExtensions 标记的空对象,三是一个所有属性都为不可配置的不可扩展对象。

freeze()

冻结对象。一个被冻结的对象再不能添加或删除属性,也不能修改已有属性的 configurable、enumerable、writable,value,此外,其原型也不能被修改。

注意: 被冻结的对象是不可变的,但如果其属性的值是一个引用类型,其该引用类型下的值是可以修改的。

isFrozen()

判断一个对象是否被冻结。默认情况下,对象是非冻结的,冻结对象一般有三种情况:一是 freeze 方法标记的对象,二是 preventExtensions 标记的空对象,三是一个所有属性都为不可修改、不可配置的不可扩展对象。

entries()

返回给定对象自身可枚举属性的 [key, value] 数组。

const obj = { a: 1, b: 2 };
console.log(JSON.stringify(Object.entries(obj))) // [["a",1],["b",2]]
keys()

返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。

注意: 在ES5里,如果此方法的参数不是对象(而是一个原始值),那么它会抛出 TypeError。在ES2015中,非对象的参数将被强制转换为一个对象。

values()

返回一个给定对象自身的所有可枚举属性值的数组,值的顺序与使用 for...in 循环的顺序相同 ( 区别在于 for...in 循环枚举原型链中的属性 )。

Javascript 版本差异:

preventExtensions()、isExtensible()、seal()、isSealed()、freeze()、isFrozen():在 ES5 中,如果这些方法的参数不是一个对象,那么它会导致 TypeError。而在 ES2015 中,表现各自不同:preventExtensions() | seal() | freeze() 将非对象参数视为一个 不可扩展 | 已被密封 | 被冻结 的普通对象,直接返回;isExtensible() | isSealed() | isFrozen() 将非对象参数将被视为一个 不可扩展 | 已被密封 | 被冻结 的普通对象,返回 false | true | true。

getOwnPropertyDescriptor()、getOwnPropertyNames()、getPrototypeOf():在 ES5 中,如果该方法的第一个参数不是对象(而是原始类型),那么就会产生出现 TypeError。而在 ES2015,第一个的参数不是对象的话就会被强制转换为对象。

Object实例属性

constructor

返回创建实例对象的构造函数。构造函数就是一个普通函数,专用于来生成实例对象。构造函数就是对象的模板,描述实例对象的基本结构。一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。

constructor 属性的作用是,可以得知某个实例对象,到底是哪一个构造函数产生的。

构造函数有以下特点:

  • 函数体内部使用了 this 关键字,代表了所要生成的对象实例。
  • 生成对象的时候,必须使用 new 命令。

注意: 对原始类型来说,如 1、true、"test",构造函数是只读的,但在其他对象中,构造函数是可修改的。

使用实例的 constructor 属性,可以从一个实例对象新建另一个实例:

function Parent() {
    this.name = 'Parent'
}
const p1 = new Parent()
console.log(p1.name) // Parent

const p2 = new p1.constructor()
console.log(p1.name) // Parent
__proto__

返回实例对象的内部 [[Prototype]] 属性,也就是构造函数的 prototype 对象。

function Parent() { }
const p1 = new Parent()

console.log(p1.constructor === Parent) // true
console.log(p1.__proto__ === Parent.prototype) // true
console.log(Object.getPrototypeOf(p1) === Parent.prototype) // true

注意: __proto__ 属性是一个访问器属性(一个 getter 和一个 setter 函数),是可读写的。

注意: __proto__ 已被大多数浏览器厂商所支持,其仅在 ES6 规范中被标准化(ES6 以前,__proto__ 并不是 EcmaScript 语言规范),是为了确保 Web 浏览器的兼容性。为了更好的支持,建议使用 Object.getPrototypeOf()

注意: 改变原型对象的属性,在每一个 JavaScript 引擎和浏览器中都是一个非常慢且影响性能的操作,原型对象的属性修改会影响到所有继承自该原型的实例对象。一般建议是,创建一个新的且可以继该原型的对象再重写对象的属性和方法,推荐使用 Object.create()

Object实例方法

hasOwnProperty()

返回一个布尔值,表示指定的属性是否是对象自身的属性(包括不同枚举属性)。

const obj = { a: 1 }
Object.defineProperty(obj, 'b', {
    value: 2,
    enumerable: false
})
console.log(obj.hasOwnProperty('a')) // true
console.log(obj.hasOwnProperty('b')) // true
console.log(obj.hasOwnProperty('toString')) // true

hasOwnProperty 是可读写的,当某个对象重写了 hasOwnProperty 属性,就需要使用外部的 hasOwnProperty 获得正确的结果:

const obj = {
    a: 1,
    hasOwnProperty() { return '自定义hasOwnProperty' }
};
console.log(obj.hasOwnProperty('a')); // '自定义hasOwnProperty'
console.log(Object.prototype.hasOwnProperty.call(obj, 'a')) // true
isPrototypeOf()

返回一个布尔值,表示一个对象是否存在于另一个对象的原型链上。

function A() { }
function B() { }
function C() { }
B.prototype = Object.create(A.prototype);
C.prototype = Object.create(B.prototype);

const baz = new C();
console.log(C.prototype.isPrototypeOf(baz));      // true
console.log(B.prototype.isPrototypeOf(baz));      // true
console.log(A.prototype.isPrototypeOf(baz));      // true
console.log(Object.prototype.isPrototypeOf(baz)); // true
propertyIsEnumerable()

返回一个布尔值,表示指定的属性是否可枚举。

toLocaleString()

返回对象的字符串表示,会返回调用 toString() 的结果。该方法用于派生对象为了特定语言环境的目的而重载使用。如:Array、Number、Date 对象会重载访方法。

toString()

返回一个表示该对象的字符串。

默认情况下,toString() 方法被每个 Object 对象继承,也可以自定义一个方法,来取代默认的 toString()。

注意: 自定义的 toString() 方法不能传入参数,并且必须返回一个字符串。

function A() {}
const a = new A()
console.log(a.toString()) // '[object Object]'

A.prototype.toString = function () {
    return '[object A]'
}
console.log(a.toString()) // '[object A]'

可用于检测对象类型,需要以 Object.prototype.call() 或者 Object.prototype.apply() 的形式来调用:

const toString = Object.prototype.toString;

console.log(toString.call(new Date));   // [object Date]
console.log(toString.call(new String)); // [object String]
console.log(toString.call(Math));       // [object Math]

console.log(toString.call('abc')); // [object String]
console.log(toString.call(1));     // [object Number]
console.log(toString.call(true));  // [object Boolean]
console.log(toString.call(Symbol('symb'))); // [object Symbol]

console.log(toString.call(undefined));  // [object Undefined]
console.log(toString.call(null));       // [object Null]

function A() { }
const a = new A()
console.log(toString.call(a)); // [object Object],无法识别自定义类型

备注: 从 JavaScript 1.8.5 开始,toString() 调用 null 返回 [object Null],undefined 返回 [object Undefined]。

valueOf()

返回指定对象的原始值。

每个内置的核心对象都会覆盖此方法以返回适当的值,如果对象没有原始值,返回对象本身:

  • String: 返回字符串值。
  • Number: 返回数字值。
  • Boolean: 返回布尔值。
  • Date: 返回是从 1970 年 1 月 1 日午夜开始计的毫秒数 UTC。
  • Object: 返回对象本身。
  • Function: 返回函数本身。
  • Array: 返回数组本身。
  • Math 和 Error 对象没有 valueOf 方法。

可以覆盖对象的 valueOf 方法,自定义的 valueOf 方法不能传入参数。

数组(Array)

数组是一种使用整数作为键属性并与长度属性关联的常规对象,此外,数组对象还继承了 Array.prototype 的一些操作数组的便捷方法。

数组的长度和元素类型都是非固定的。本质上,数组是以数值为键名,且有 length 属性的一种特殊的对象。JavaScript 语言规定,对象的键名一律为字符串,所以,数组的键名其实也是字符串。之所以可以用数值读取,是因为非字符串的键名会被转为字符串。

还有一种类似数组的对象,即对象的所有键名都是正整数或零,并且有 length 属性(非动态值,不会随着成员的变化而变化),但不是真正的数组,没有继承 Array.prototype 的方法。比如:document.querySelectorAll() 方法的返回值、函数体内的 Arguments 变量。

数组的 slice 方法可以将 “类似数组的对象” 变成真正的数组:

Array.prototype.slice.call(arrayLike);

Array对象

Array 是 Javascript 内置的一个标准对象,同时也是一个构造函数,可以用它生成新的数组。注意: 该构造函数传不同的参数,会导致它的行为不一致。

// 不传参数
new Array()  // []
new Array(0) // []

// 传一个正整数
new Array(2) // [empty × 2]

// 传一个非正整数
new Array(1.1) // Uncaught RangeError: Invalid array length
new Array(-1)  // Uncaught RangeError: Invalid array length

// 传一个非数值(比如字符串、布尔值、对象等)
new Array('a') // ['a']
new Array([])  // [Array(0)]

// 传多个参数时
new Array(1, 2, 3) // [1, 2, 3]
new Array(1, 'a', true) // [1, 'a', true]

注意: 如果参数是一个正整数,返回数组的成员都是空位。虽然读取的时候返回 undefined,但元素是 undefined 的数组不同。如果是空位,使用数组的 forEach 方法、for...in 结构、以及 Object.keys 方法进行遍历,空位都会被跳过。

此外,当数组中,两个逗号之间没有任何值或者使用 delete 命令删除一个数组成员,会形成空位(hole)。空位是包括在 length 属性中的,也就是说,删除一个数组成员不会影响 length 属性。

const arr1 = new Array(2);
const arr2 = [undefined, undefined]
console.log(arr1[0]) // undefined
console.log(arr2[0]) // undefined
console.log(arr1.length) // 2
console.log(arr2.length) // 2
console.log(Object.keys(arr1)) // []
console.log(Object.keys(arr2)) // ['0', '1']

Array静态方法

from()

对一个类似数组或可迭代对象创建一个新的、浅拷贝的数组。

Array.from(arrayLike[, mapFn[, thisArg]])
const obj = {
    0: 'a',
    1: 'b',
    2: 'c',
    length: 3
}
// obj.push('d') // Uncaught TypeError: obj.push is not a function

console.log(Array.from(obj, function (ele, i) {
  return `[${i}]:${ele}${this.suffix}`
}, { suffix: '!' }))
// ['0-a!', '1-b!', '2-c!']
isArray()

判断传递的参数是否是一个数组对象。

注意: 鲜为人知的事实:其实 Array.prototype 也是一个数组。

Array.isArray(Array.prototype); // true
of()

将一组参数,转换为数组,支持任意的参数数量和类型。

of() 和 Array 构造函数的区别:只有一个正整数传参时,of() 当参数当作一个子元素,而 Array 构造函数会当作数组的长度,生成对应数量的空位。

Array.of(3)       // [3]
Array.of(1, 2, 3) // [1, 2, 3]

Array(3)       // [empty × 3]
Array(1, 2, 3) // [1, 2, 3]

Array实例属性

length

返回或设置一个数组中的元素个数,是一个无符号 32-bit(0 ~ 2^32 - 1)整数。如果设置 length 值小于数组元素的个数,将会截断数组;如果设置 length 值大于数组元素的个数,则会增加元素个数,新增元素的位置都是空位。

const arr = [1, 2, 3, 4]
arr.length = 2
console.log(arr) // [1, 2]
arr.length = 10
console.log(arr) // [1, 2, empty × 8]
[@@unscopables]

返回所有 ES6 中新定义的、且并未被更早的 ECMAScript 标准收纳的属性名。这些属性被排除在由 with 语句绑定的环境中。

Array.prototype[Symbol.unscopables]
/*****
{
  "copyWithin": true,
  "entries": true,
  "fill": true,
  "find": true,
  "findIndex": true,
  "flat": true,
  "flatMap": true,
  "includes": true,
  "keys": true,
  "values": true,
  "at": true,
  "findLast": true,
  "findLastIndex": true
}
*****/

Array实例方法

concat()

用于合并两个或多个数组。不改变原数组,返回原数组浅拷贝的新数组

如果参数不是数组,则将参数做为原数组的一个元素,添加到数组尾部。

const arr = [1, 2, 3, 4]
arr.concat([5, 6, 7, 8]) // [1, 2, 3, 4, 5, 6, 7, 8]
arr.concat(9, 10)        // [1, 2, 3, 4, 9, 10]
arr.concat(true)         // [1, 2, 3, 4, true]
arr.concat({})           // [1, 2, 3, 4, {}]
copyWithin()

浅拷贝数组的一部分到同一数组中的另一个位置,并返回它,不会改变原数组的长度。改变原数组

arr.copyWithin(index[, start[, end]])

index 表示将拷贝序列到该位置。如果是负数,则从末尾开始计算;如果 大于等于数组长度,则不拷贝。

start 表示开始复制元素的起始位置。如果是负数,则从末尾开始计算;如果忽略,则默认为 0。

end 表示开始复制元素的结束位置(不包括 end 位置的元素)。如果是负数, 则从末尾开始计算;如果忽略,则默认为数组长度。如果 start 小于 end,则不拷贝。

[1, 2, 3, 4, 5, 6, 7, 8].copyWithin(8);       // [1, 2, 3, 4, 5, 6, 7, 8],不拷贝
[1, 2, 3, 4, 5, 6, 7, 8].copyWithin(0, 7, 8); // [8, 2, 3, 4, 5, 6, 7, 8]
[1, 2, 3, 4, 5, 6, 7, 8].copyWithin(-2);      // [1, 2, 3, 4, 5, 6, 1, 2]
[1, 2, 3, 4, 5, 6, 7, 8].copyWithin(0, -2);   // [7, 8, 3, 4, 5, 6, 7, 8]
[1, 2, 3, 4, 5, 6, 7, 8].copyWithin(3, 0);    // [1, 2, 3, 1, 2, 3, 4, 5]

[1, 2, 3, 4, 5, 6, 7, 8].copyWithin(0, 6, 4); // [1, 2, 3, 4, 5, 6, 7, 8],不拷贝

备注: copyWithin() 是用来移动 Array 或者 TypedArray 数据的一个高性能的方法。

fill()

用一个固定值填充一个数组中从起始索引到终止索引(不包括终止索引)内的全部元素。改变原数组

arr.fill(value[, start[, end]])
[1, 2, 3].fill(4);         // [4, 4, 4]
[1, 2, 3].fill(4, 1);      // [1, 4, 4]
[1, 2, 3].fill(4, 1, 2);   // [1, 4, 3]
[1, 2, 3].fill(4, -3, -2); // [4, 2, 3]
entries()

返回一个包含数组中每个元素键/值对的 Array 迭代器对象。

const arr = ["a", "b", "c"];

const iterator = arr.entries();
console.log(iterator); // Array Iterator {}
for (let i = 0; i < arr.length; i++) {
    const tem = iterator.next();
    console.log(tem.value);
}
// [0, 'a']
// [1, 'b']
// [2, 'c']

for (let ele of arr.entries()) {
    console.log(ele);
}
// [0, 'a']
// [1, 'b']
// [2, 'c']
keys()

返回一个包含数组中每个元素的 Array 迭代器对象。

values()

返回一个包含数组中每个元素的 Array 迭代器对象。

every()

返回一个布尔值,判断数组内是否所有元素都能通过某个指定函数的测试。

arr.every(callback(element[, index[, array]])[, thisArg])

注意: 该方法有两个参数,第一个 callback 是测试每个元素的函数,第二个参数是 thisArg 函数执行时使用的 this。

const arr = [1, 2, 3, 4];
const bool = arr.every(function (ele, index, array) {
    return ele < this.value
}, {
    value: 5
})
console.log(bool);

注意: 如果是一个空数组,此方法在任何情况下都会返回 true

console.log([].every((ele) => ele < 100))   // true
console.log([].every((ele) => ele === 100)) // true
console.log([].every((ele) => ele > 100))   // true
some()

返回一个布尔值,判断数组内是否至少有一个元素能通过某个指定函数的测试。

注意: 如果是一个空数组,此方法在任何情况下都会返回 false

filter()

返回一个包含满足所提供的测试函数的所有元素的新数组不改变原数组,返回原数组浅拷贝的新数组

arr.filter(callback(element[, index[, array]])[, thisArg])

注意: 遍历的元素范围在第一次调用 callback 之前就已经确定了,之后新增到数组的元素不会被遍历到。如果已经存在的元素被改变了,则传入 callback 的值是遍历到它们那一刻的值。在遍历时被删除的元素,不会被遍历到。

const arr = [1, 2, 3, 4]
arr.filter((ele) => {
    delete arr[1] // 删除元素,不会被遍历到
    arr[2] = 100  // 修改元素,传入 callback 的值是修改后的
    arr[4] = 5    // 新增元素,不会被遍历到
    console.log(ele)
})
//依次打印:1 100 4
find()

返回数组中满足所提供的测试函数的第一个元素的,否则返回 undefined。不改变原数组,返回原数组浅拷贝的新数组

arr.find(callback(element[, index[, array]])[, thisArg])

注意: 该方法会遍历空位元素。

const arr = new Array(4) // [empty × 4]
arr.find((ele, index) => {
    console.log(index)
    return !!ele
})
// 依次打印: 1 2 3 4
findIndex()

与 find() 类似,返回的是数组中满足所提供的测试函数的第一个元素的索引,否则返回 -1。

flat()

按照一个指定的深度递归遍历数组,并将该数组所有元素以及其子数组中的元素合并,作为一个新数组返回。默认深度是 1。不改变原数组,返回原数组浅拷贝的新数组

const arr = [
    0,
    1,
    [3, [3, 5]]
];
console.log(arr.flat())  // [0, 1, 3, Array(2)]
console.log(arr.flat(2)) // [0, 1, 3, 3, 5]
flatMap()

与 深度为 1 的 flat() 类似,但会先对每个元素执行一个类似 Map 的函数。

const arr = [1, 2, 3];
console.log(arr.flatMap(ele => ele * 2))        // [2, 4, 6]
console.log(arr.flatMap(ele => [ele, ele * 2])) // [1, 2, 2, 4, 3, 6]
forEach()

对数组的每个元素执行一次给定的函数。

arr.forEach(callback(element[, index[, array]])[, thisArg])
map()

对数组的每个元素执行一次给定的函数,并返回一个由每次执行函数后的返回值组成的新数组

reduce()

对数组中的每个元素按从左往右的顺序执行指定的函数,每一次运行函数会将先前元素的计算结果作为第一个参数传入,最后返回汇总结果。

arr.reduce(callback(prev, current[, currentIndex[, array]])[, initialValue])

initialValue 是第一次调用函数时,参数 prev 的值。如果指定了 initialValue,则 current 将使用数组第一个元素;否则 prev 将使用数组第一个元素,而 current 将使用数组第二个元素。

const arr = [1, 2, 3, 4];
const temp = []
arr.reduce((prev, current) => temp.push(current))
console.log(temp) // [2, 3, 4]

const temp2 = []
arr.reduce((prev, current) => temp2.push(current), 0)
console.log(temp2) // [1, 2, 3, 4]

如果数组为空且未指定初始值 initialValue,则会抛出 TypeError。

如果数组仅有一个元素并且没有提供 initialValue,或者有提供 initialValue 但是数组为空,那么此唯一值将被返回且函数不会被执行。

[].reduce((prev, current) => {});
// Uncaught TypeError: Reduce of empty array with no initial value

const temp = [];
[].reduce((prev, current) => temp.push(current), 100)
console.log(temp) // []

const temp2 = [];
[100].reduce((prev, current) => temp2.push(current))
console.log(temp2) // []

示例: Promise 顺序执行。

const sequrencePromise = (arr, initialValue) => arr.reduce(
    (chain, current) => chain.then(current),
    Promise.resolve(initialValue)
)

const p1 = (a) => new Promise((resolve) => { resolve(a * 1) })
const p2 = (a) => new Promise((resolve) => { resolve(a * 2) })
const f3 = (a) => a * 3
const p4 = (a) => new Promise((resolve) => { resolve(a * 4) })

sequrencePromise([p1, p2, f3, p4], 10).then(console.log) // 240
reduceRight()

与 reduce() 类似,区别是对数组的遍历顺序是从右往左

includes()

返回一个布尔值,判断一个数组是否包含一个指定的值。

arr.includes(value[, startIndex])

注意: 特殊数值 +0 和 -1、NaN 和 NaN 的判断。

[+0].includes(-0);   // true
[NaN].includes(NaN); // true
indexOf()

返回在数组中找到给定元素的第一个索引,如果不存在,则返回 -1。该方法使用严格相等(===)来比较元素。

arr.indexOf(value[, startIndex])
[+0].indexOf(-0);   // 0
[NaN].indexOf(NaN); // -1
lastIndexOf()

与 indexOf() 类似, 返回在数组中找到给定元素的最后一个索引,如果不存在,则返回 -1。

join()

将一个数组或类数组的所有元素用指定分隔符连接成一个字符串,并返回这个字符串。默认的分隔符是逗号(,)。

arr.join([separator])

注意: 如果一个元素为 undefined 或 null,它会被转换为空字符串。

[1, 2, 3, true, undefined, null].join('-') // '1-2-3-true--'
pop()

删除数组中最后一个元素,并返回该元素的值。如果空数组调用该方法,返回 undefined。改变原数组

push()

将一个或多个元素添加到数组的末尾,并返回该数组的新长度。改变原数组

shift()

删除数组中第一个元素,并返回该元素的值。如果空数组调用该方法,返回 undefined。改变原数组

unshift()

将一个或多个元素添加到数组的开头,并返回该数组的新长度。改变原数组

splice()

删除、替换、添加数组中的元素,返回一个由被删除的元素组成的一个数组,如果没有删除元素,则返回空数组。 会改变原数组

array.splice(start[, deleteCount[, item1[, item2[, ...]]]])

start 指定修改的开始位置。如果超出了数组的长度,则从数组末尾开始添加内容;如果是负值,则表示从数组末位开始的第几位;如果负数的绝对值大于数组的长度,则表示开始位置为第 0 位。

deleteCount 表示要移除的数组元素的个数。如果省略或者大于 start 之后的元素的总数,则删除 start 及之后面的元素;如果是 0 或者负数,则不移除元素。这种情况下,至少应添加一个新元素。

item1, item2, ... 表示要添加进数组的元素,从 start 位置开始。如果不指定,则将只删除数组元素。

const arr = [1, 2, 3, 4, 5, 6];
// console.log(arr.splice(3))  // [4, 5, 6]
// console.log(arr)            // [1, 2, 3]

// console.log(arr.splice(-1)) // [6]
// console.log(arr)            // [1, 2, 3, 4, 5]

console.log(arr.splice(10, 1, 7, 7)) // []
console.log(arr)                     // [1, 2, 3, 4, 5, 6, 7, 7]

注意: 如果 start、deleteCount 是小数,则会忽略小数部分??如果不是有效数值,则默认为 0。

const arr = [1, 2, 3, 4, 5, 6];
// console.log(arr.splice(3.5)) // [4, 5, 6]
// console.log(arr)             // [1, 2, 3]

// console.log(arr.splice('a')) // [1, 2, 3, 4, 5, 6]
// console.log(arr)             // []

console.log(arr.splice(0, 'b', 7, 7)) // []
console.log(arr)                      // [7, 7, 1, 2, 3, 4, 5, 6]
slice()

返回一个新的数组,新数组是原数组指定范围(包括 begin,但不包括 end)的浅拷贝。不改变原数组,返回原数组浅拷贝的新数组

arr.slice([begin[, end]])

begin 起始索引。如果为负数,表示从原数组中的倒数第几个元素开始提取;如果省略,则默认为 0;如果大于数组的长度,则返回空数组。

end 终止索引。如果为负数, 表示在原数组中的倒数第几个元素结束抽取;如果省略,则会一直提取到原数组末尾;如果大于数组的长度,也会一直提取到原数组末尾。

如果 begin 大于 end,或者 begin,或者 end 参数无效,则返回空数组。

console.log(arr.slice())       // [1, 2, 3, 4, 5, 6]
console.log(arr.slice(2))      // [3, 4, 5, 6]
console.log(arr.slice(2, 5))   // [3, 4, 5]
console.log(arr.slice(4, 2))   // []
console.log(arr.slice(0, 'b')) // []

slice 方法可以用来将一个类数组对象/集合转换成一个新数组:

Array.prototype.slice.call(arguments);
[].slice.call(arguments)
reverse()

将数组反序排序,并返回该数组。改变原数组

sort()

用原地算法对数组的元素进行排序,并返回数组。该方法接收一个用来指定按某种顺序进行排列的函数。如果省略,元素按照转换为的字符串的各个字符的 Unicode 位点进行排序。改变原数组

arr.sort([compareFunction])
[1, 2, 100, 3, 256].sort()
// [1, 100, 2, 256, 3]。注意:不是根据数值,而是 UniCode 位点排序

如果指明了 compareFunction ,那么数组会按照调用该函数的返回值排序:

  • compareFunction(a, b) < 0,那么 a 会被排列到 b 之前。
  • compareFunction(a, b) === 0, a 和 b 的相对位置不变。备注: ECMAScript 标准并不保证这一行为,而且也不是所有浏览器都会遵守(例如:Mozilla 在 2003 年之前的版本);
  • compareFunction(a, b) > 0,b 会被排列到 a 之前。

compareFunction(a, b) 必须总是对相同的输入返回相同的比较结果,否则排序的结果将是不确定的。

const arr = [1, 2, 100, 3, 256, 4, 23, 59, 11, 89, 8];
arr.sort((a, b) => a - b); // [1, 2, 3, 4, 8, 11, 23, 59, 89, 100, 256],递增
arr.sort((a, b) => b - a); // [256, 100, 89, 59, 23, 11, 8, 4, 3, 2, 1],递减

注意: 自 ES10(EcmaScript 2019)起,ECMAScript 标准规范要求 Array.prototype.sort 为稳定排序。

注意: 该方法取决于浏览器具体实现,因此无法保证排序的时间和空间复杂性。

V8 引擎对 sort 方法提供了 2 种排序算法:插入排序、快速排序。

var InsertionSort = function InsertionSort(a, from, to) {
    for (var i = from + 1; i < to; i++) {
        var element = a[i]; // 当前排序元素
        for (var j = i - 1; j >= from; j--) {
            var tmp = a[j]; // 当前排序元素前的元素
            var order = comparefn(tmp, element);
            if (order > 0) { // 当前排序元素前的元素大于当前元素,则换位置
                a[j + 1] = tmp;
            } else {
                break;
            }
        }
        a[j + 1] = element;
    }
};

当数组长度小于等于 10 的时候,采用插入排序,大于 10 的时候,采用快速排序。当长度大于 1000 的数组,采用的是快速排序与插入排序混合的方式进行排序的,因为,当数据量很小的时候,插入排序效率优于快速排序。

chromium V8 引擎 array.js 源码

toString()

返回一个字符串,表示指定的数组及其元素。

toLocaleString()

返回一个字符串表示数组中的元素。数组中的元素(Object、Date、Number)将使用各自的 toLocaleString 方法转成字符串,这些字符串将使用一个特定语言环境的字符串格式。

const arr = [10000, 'a', new Date('21 Dec 1997 14:12:00 UTC')];
const localeString = arr.toLocaleString('zh-Hans', {
    timeZone: 'UTC',
    style: 'currency',
    currency: 'CNY'
});

console.log(localeString); // ¥10,000.00,a,1997/12/21 14:12:00
@@iterator

返回值数组的迭代器方法,默认情况下,与 values() 方法指向同一函数。

const arr = [1, 2, 3, 4];
console.log(arr[Symbol.iterator] === arr.values); // true

concat()、filter()、find()、flat()、slice():不改变原数组,返回原数组浅拷贝的新数组。

copyWithin()、pop()、push()、shift()、unshift()、splice()、reverse()、sort():改变原数组。

类型数组(Typed Arrays)

JavaScript 类型数组是类似数组的对象,它提供了一种在内存缓冲区中读取和写入原始二进制数据的机制。

Array 对象动态增长和收缩,并且可以具有任何 JavaScript 值。然而,随着 Web 应用程序变得越来越强大,添加了诸如音频和视频操作、使用 WebSocket 访问原始数据等功能,很明显,这时需要 JavaScript 代码能够快速轻松地操作原始二进制数据。这就是类型数组的用武之地。

类型数组是 ECMAScript 2015 中新定义的 JavaScript 内建对象,它的每个条目都是原始二进制值,采用多种支持格式的数值,从 8 位整数到 64 位浮点数。

为了实现最大的灵活性和效率,JavaScript 类型化数组将实现拆分为缓冲区和视图。缓冲区(由 ArrayBuffer 对象实现)是表示数据块的对象;它没有可言的格式,也没有提供访问其内容的机制。视图提供了一个上下文——即数据类型、起始偏移量和元素数量——将数据转换为类型化数组。

缓冲区: ArrayBuffer 是一种数据类型,用于表示通用的、固定长度的二进制数据缓冲区,是一个字节数组,通常在其他语言中称为 “byte array”。

new ArrayBuffer(length)

length 是要创建的 ArrayBuffer 的大小,单位为字节。如果 length 大于 Number.MAX_SAFE_INTEGER(>= 2 ** 53)或为负数,则抛出一个 RangeError 异常。

返回值是一个指定大小的 ArrayBuffer 对象,其内容被初始化为 0。

视图: ArrayBuffer 的内容需要类型数组视图或 DataView 对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。

类型数组指的是以下的其中之一:

  • Int8Array();
  • Uint8Array();
  • Uint8ClampedArray();
  • Int16Array();
  • Uint16Array();
  • Int32Array();
  • Uint32Array();
  • Float32Array();
  • Float64Array();
const buffer = new ArrayBuffer(16);       // 16bytes * 8 = 128bits
const int32View = new Int32Array(buffer); // 每条记录32bits
console.log(int32View.length)             // 4, 128bits / 32bits
for (let i = 0; i < int32View.length; i++) {
    int32View[i] = i * 2;
}
console.log(JSON.stringify(int32View))
// Int32Array(4) [0, 2, 4, 6, buffer: ArrayBuffer(16), byteLength: 16, byteOffset: 0, length: 4, Symbol(Symbol.toStringTag): 'Int32Array']

const int16View = new Int16Array(buffer); // 每条记录16bits
console.log(int16View.length)             // 8, 128bits / 16bits = 8
console.log(int16View)
// Int16Array(8) [0, 0, 2, 0, 4, 0, 6, 0, buffer: ArrayBuffer(16), byteLength: 16, byteOffset: 0, length: 8, Symbol(Symbol.toStringTag): 'Int16Array']

函数(Function)

函数是 JavaScript 中的基本组件之一,一个函数是 JavaScript 中一组可重复执行任务或计算值的语句。

注意: 当一个函数是一个对象的属性时,称之为方法。

定义函数

JavaScript 有三种定义函数的方法:

函数声明

一个函数定义(也称函数声明、函数语句)由一系列的 function 关键字组成,依次为:

  • 函数的名称。
  • 函数参数列表,包围在括号(())中并由逗号分隔。
  • 定义函数的 JavaScript 语句,用大括号 ({}) 括起来。
function sum(a, b) {
    return a * b;
}
函数表达式

函数也可以由函数表达式创建。这样的函数可以有名称(用于在函数内部代指其本身,或者在调试器堆栈跟踪中识别该函数),也可以没有名称。

const sum1 = function (a, b) { return a * b; }
const sum2 = function fName (a, b) { return a * b; }

console.log(sum1.name) // sum1
console.log(sum2.name) // fName
Function 构造函数

也可以用 Function 构造器由一个字符串来创建一个函数。

const sum = new Function('a', 'b', 'return a * b')
console.log(sum.name) // 'anonymous'

Function 构造函数可以接收任意个参数,除了最后一个当作函数数,其余都作为函数参数名。

Function 构造函数可以不使用 new 命令,返回结果完全一样。

注意: 如果函数定义中有同名的参数,则取最后出现的那个值。

Function 构造函数与函数声明之间的不同

由 Function 构造函数创建的函数不会创建当前环境的闭包,它们总是被创建于全局环境,因此,在运行时它们只能访问全局变量和自己的局部变量,不能访问它们被 Function 构造函数创建时所在的作用域的变量。

这也是与使用 eval() 执行创建函数的主要不同点。

函数作用域

什么是作用域?

一句话:作用域就是变量与函数的可访问范围。

作用域是一个抽象概念,在 JavaScript 中, 作用域可以理解为可访问变量、对象、函数的集合。通常我们说某个变量在不在某个作用域,指的就是变量在此作用域是否可访问。换句话说,作用域决定了代码区块中变量和其他资源的访问权限。

在函数内定义的变量不能在函数之外的任何地方访问,因为变量仅仅在该函数的域的内部有定义。相对应的,一个函数可以访问定义在其范围内的任何变量和函数。换言之,定义在全局域中的函数可以访问所有定义在全局域中的变量。在另一个函数中定义的函数也可以访问在其父函数中定义的所有变量和父函数有权访问的任何其他变量。

Javascript 有三种作用域:全局作用域、函数作用域、ES6 新增的块级作用域。

  • 全局作用域: 也称为全局变量,是指能被程序中任何函数或者方法访问的变量或函数。全局变量的生命周期是存在于整个程序之内,只有在页面关闭后才会销毁。
  • 函数作域: 也称为局部变量,是指函数内声明的变量,只能在函数内部访问,外层的作用域(全局作用域或者其他函数作用域)无法直接访问。局部变量的生命周期是存在于函数执行期间,函数执行完毕后销毁。

  • 块级作用域: 是指通过 ES6 新增命令 let 和 const 在块级语句(大括号 {} 中的语句))中声明的变量,其变量在指定块的作用域外无法被访问。

arguments对象

arguments 对象用于存储函数在动行时的实际参数,是一个类数组对象。

function func(a, b) {
    console.log(arguments)
}
func(2, 5)
/**
Arguments(2) [2, 5, callee: ƒ, Symbol(Symbol.iterator): ƒ]
0: 2
1: 5
callee: ƒ func(a, b)
length: 2
Symbol(Symbol.iterator): ƒ values()
[[Prototype]]: Object
**/

callee 指向参数所属的当前执行的函数;length 是指传递给函数的参数数量;[@@iterator] 返回一个新的 Array 迭代器对象,该对象包含参数中每个索引的值。

严格模式下,arguments 对象与函数参数不具有联动关系,也就是说,修改 arguments 对象不会影响到实际的函数参数。

function func(a, b) {
    arguments[1] = 10
    return a + b
}
func(1, 2) // 11
"use strict";
function func(a, b) {
    arguments[1] = 10
    return a + b
}
func(1, 2) // 3

注意: 箭头函数下,不支持 arguments 对象,建议使用剩余参数的形式接收参数。

const func = (a, b) => {
    console.log(arguments)
}
func(2, 5)
// Uncaught ReferenceError: arguments is not defined
const func = (...args) => {
    console.log(args)
}
func(2, 5)

注意: arguments 对象是 类数组,就是说 arguments 有长度属性,并且属性的索引是从零开始的,但是它没有 Array 的内置方法,如:forEach()、map() 等。

可以通过以下方法,将类数组转换为数组:

Array.prototype.slice.call(arguments);
[].slice.call(arguments)

Array.from(arguments);
[...arguments];

闭包(closure)

谈谈 JavaScript 的作用域和上下文#闭包

闭包是 JavaScript 中最强大的特性之一。

一个函数里面嵌套另外一个函数,被嵌套(内部)的函数自身形成了一个闭包,这个闭包可以自己拥有独立的词法环境,也就意味着,被嵌套函数的作用域除了自身的作用域,还维持了其容器函数的作用域。

此外,由于被嵌套函数可以访问容器函数的作用域,因此当被嵌套函数生存周期大于外部函数时,容器函数中定义的变量和函数的生存周期将比被嵌套函数执行时间长。当被嵌套函数以某一种方式被任何一个外部作用域访问时,一个闭包就产生了。

一句话概括:闭包就是一个拥有自身作用域和父级作用域(由外层函数提供,即便外层函数销毁依旧可以访问)的特殊函数。

注意: 闭包的本质是利用了作用域的机制,来达到外部作用域访问内部作用域的目的。

let func = function () {
    let name = 'lizh';
    function f1() {
        console.log(name)
    }
    return f1
}
const func2 = func(); // func函数已执行完毕,执行上下文被释放
func2() // 打印:lizh。也就是,func函数执行上下文被释放后,func2函数仍能访问其内的变量

func = null // 即使 func 销毁
func2()     // 仍能访问 func 函数内的变量

即使 func 函数执行上下文被释放后,甚至 func 函数被销毁,f1 函数仍能访问 func 作用域内的变量,无论 f1 是在什么作用域下调用。

Function对象

Function 是 Javascript 内置的一个标准对象,也是一个创建函数的构造函数。每个 JavaScript 函数实际上都是一个 Function 对象。

new Function('a', 'b', 'return a + b')

Function 构造函数可以接收任意个参数,除了最后一个当作函数数,其余都作为函数参数名。

Function 构造函数可以动态创建函数,但会遇到和 eval() 类似的的安全问题和性能问题(相对较小的)。然而,与 eval() 不同的是,Function 构造函数创建的函数只能在全局作用域中运行。

eval() 是一个危险的函数。如果 eval() 运行的字符串代码被恶意方(不怀好意的人)修改,最终可能会在客户端上运行恶意代码。更重要的是,第三方代码可以看到某一个 eval() 被调用时的作用域,这也有可能导致一些不同方式的攻击。

相似的 Function 就不容易被攻击。

Function实例属性

length

返回函数的形参个数。

注意: 形参个数不包括剩余参数,且仅返回第一个具有默认值之前的参数个数。

console.log((function () { }).length);     // 0
console.log((function (a) { }).length);    // 1
console.log((function (a, b) { }).length); // 2

console.log((function (...args) { }).length);     // 0
console.log((function (a, ...args) { }).length);  // 1
console.log((function (a, b = 1, c) { }).length); // 1
name

返回函数实例的名称,是只读属性。

Function.bind() 所创建的函数将会在函数的名称前加上 "bound " 。

function func() { }
const f1 = func.bind()
console.log(f1.name) // 'bound func'

当通过 get 和 set 访问器来存取属性时, "get" 或 "set" 会出现在函数名称前。

const obj = {
    get foo() { },
    set foo(x) { }
};

const descriptor = Object.getOwnPropertyDescriptor(obj, "foo");
descriptor.get.name; // 'get foo'
descriptor.set.name; // 'set foo'

Function实例方法

apply()

调用一个函数,并将其 this 指向为函数中的第一个参数,原函数中的参数以一个数组(或类数组对象)的形式提供。

var a = 1
function func(b, c) { return this.a + b + c }
const obj = { a: 10 }
func.apply(obj, [1, 2]) // 13
const numbers = [5, 6, 2, 3, 7];

Math.max.apply(null, numbers); // 7
// 相当于
Math.max(...numbers); // 7

Array.prototype.push.apply(numbers, [10, 20, 30])
// 相当于
numbers.push(10, 20, 30)
console.log(numbers) // [5, 6, 2, 3, 7, 10, 20, 30]
call()

调用一个函数,并将其 this 指向为函数中的第一个参数,原函数中的参数以参数列表的形式提供。

注意: call() 与 apply() 方法类似,只有一个区别,call() 方法接受的是一个参数列表,而 apply() 方法接受的是一个数组(或类数组对象)

var a = 1
function func(b, c) { return this.a + b + c }
const obj = { a: 10 }
func.call(obj, 1, 2) // 13
bind()

创建一个新的函数。在新函数被调用时,this 指定为函数中的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

var a = 1
function func(b, c) { return this.a + b + c }
const obj = { a: 10 }
const f1 = func.bind(obj, 1)
f1(2) // 13
toString()

返回一个表示当前函数源代码的字符串。

箭头函数

箭头函数表达式的语法比函数表达式更简洁,并且没有自己的 this、arguments、super 或 new.target。

箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。

const func = (a, b) => a + b
// 相当于
const func = function (a, b) {
    return a + b
}

箭头函数的特性:

  • 不会创建自己的 this,只会从自己的作用域链的上一层继承 this。
  • call()、apply()、bind() 无法改变 this 的指向。
  • 函数内不绑定 Arguments 对象。
  • 不能用作构造器,和 new 一起用会抛出错误。
  • 没有 prototype 属性。
  • 函数内不能使用 yield 关键字。
const func = (a, b) => a + b

new func(1, 2) // Uncaught TypeError: func is not a constructor
func.prototype // undefined

Map和WeakMap对象

ECMAScript 2015 引入了一个新的数据结构来将一个值映射到另一个值。一个 Map 对象就是一个简单的键值对映射集合。Map 数据结构中,任何值(对象或者原始值)都可以作为一个键或一个值,并且能够记住键的原始插入顺序——for...of 会按照这个顺序遍历。

const map = new Map();
const obj = { a: 'a' }
map.set('b', 1)
map.set(obj, 2)
console.log(map) // Map(2) {'b' => 1, {…} => 2}

注意: Map 的键的比较基于零值相等算法,即 +0 === -0。

const map = new Map();
const obj = { a: 'a' }
map.set(+0, 1)
console.log(map.get(+0)) // 1
console.log(map.get(-0)) // 1

Object和Map比较

Object 和 Map 类似的是,它们都允许你按键存取一个值、删除键、检测一个键是否绑定了值。

两者区别:

  • Object 对象的键只能是字符串、Symbol 类型,而 Map 对象的键不限类型。
  • Object 对象默认情况包含原型链上的键名,而 Map 对象默认情况不包含任何键,只包含显式插入的键。
  • Object 的键目前是有序的,但这个顺序是复杂的,且不可靠,最好不要依赖属性的顺序,Map 对象的键是有序的,且总是以插入的顺序返回键值。
  • Object 对象没有内置迭代对象,是不可迭代的,比如,不使使用 for...of 循环,而 Map 对象是可迭代的。
  • Object 对象可由 JSON 对象序列化和解析,而 Map 没有序列化和解析的支持。
  • 在频繁增删键值对的场景,Map 对象的表现更好,Object 对象未作出优化。

Map对象

Map 是 Javascript 内置的一个对象,也是创建 Map 对象的构造函数。

Map 构造函数的参数接收一个数组或者其他可迭代对象,其元素为键值对(两个元素的数组)。如果参数为空、为 null、为 undefined,则返回一个空 Map 对象;如果参数不是迭代对象,则会报错。

const map = new Map([
    [1, 1],
    ['a', 'a'],
    [null, null],
    [undefined, undefined],
])
console.log(map) // Map(4) {1 => 1, 'a' => 'a', null => null, undefined => undefined}

new Map(null)      // Map(0) {size: 0}
new Map(undefined) // Map(0) {size: 0}

new Map({
    a: 1,
    b: 2
})
// Uncaught TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))

Map实例属性

  • size: 返回 Map 对象的成员数量,是只读属性。

Map实例方法

  • clear(): 移除 Map 对象中所有的键值对。
  • delete(): 移除 Map 对象中指定的键值对,如果键值对存在,返回 true,否则返回 false。
  • get(): 返回 Map 对象中指定的键,如果键值对不存在,则返回 undefined。
  • has(): 返回一个布尔值,判断 Map 对象中是否存在指定的键。
  • set(): 为 Map 对象添加或更新一个指定了键和值的(新)键值对,返回 Map 对象。
  • entries(): 返回一个新的迭代对象,是一个包含 Map 对象中所有键值对的数组,并以插入 Map 对象的顺序排列。
  • keys(): 返回一个新的迭代对象,是一个包含 Map 对象中所有的数组,并以插入 Map 对象的顺序排列。
  • values(): 返回一个新的迭代对象,是一个包含 Map 对象中所有的数组,并以插入 Map 对象的顺序排列。
  • [@@iterator]: 返回一个新的迭代对象,是一个包含 Map 对象中所有键值对的数组,并以插入 Map 对象的顺序排列。[@@iterator]() 方法与 entries() 方法返回同一个迭代对象。
  • forEach(): 按照插入顺序依次对 Map 中每个键/值对执行一次给定的函数,可以指定第二个参数,在给定的函数内,当 this 使用。与 Array.prototype.forEach() 方法类似。

WeakMap对象

WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合。

WeakMap 与 Map 的区别有:

  • WeakMap 对象的键必须是对象类型,值可以是任意类型。

  • WeakMap 对象的是弱引用,也就是说,当键所指对象没有其他地方引用时,它会被垃圾回收机制释放。

    WeakMap 对象可以防止内存泄漏,具体可查看 Javascript强引用和弱引用

    let refer1 = { a: 1 }
    const m1 = new Map()
    m1.set(refer1, 1)
    
    refer1 = null // 清除引用,但其指向对象还在内存中
    for (let value of m1.values()) { // 仍可用 m1.values() 来获取之前的值
        console.log(value) // 1
    }
    
    let refer2 = { a: 1 }
    const m2 = new WeakMap()
    m2.set(refer2, 2)
    
    refer2 = null // 清除引用,其指向对象也会自动释放
    // m2 的 refer2 键自动消失
    
  • WeakMap 对象的键是不可枚举的。造成这个问题的原因正是弱引用,其键所对应的对象可能会在将来消失,这是不可预知的。

WeakMap 提供的 API 使用与 Map 相同,但它没有遍历操作(也就是,没有 keys()、values()、entries()、forEach())的方法,也没有 clear() 方法和没有 size 属性。

Set和WeakSet对象

set 是 ES6 提供了新的数据结构。Set 对象是一组值的集合,这些值是不重复的(即 Set 中的元素是唯一的),可以按照添加顺序来遍历。类似于数组。

Array和Set比较

一般情况下,在 JavaScript 中使用数组来存储一组元素,而 Set 对象有这些优势:

  • Set 对象存储不重复的值。
  • Array.prototype.indexOf() 方法效率低下,且无法找到 NaN 值。
  • Set 对象允许根据值删除元素,而 Array 对象必须使用基于下标的 splice() 方法。

Set对象

Set 是 Javascript 内置的一个标准对象,也是创建 Set 对象的构造函数。

Set 构造函数的参数接收一个可迭代对象,参数中的所有元素将不重复地被添加到新的 Set中。如果参数为空、为 null、为 undefined,则返回一个空 Set 对象;如果参数不是迭代对象,则会报错。

new Set([1, 1, 'a', true, null, 'a', undefined, Symbol('symb')])
// Set(6) {1, 'a', true, null, undefined, …}

new Set()     // Set(0) {size: 0}
new Set(null) // Set(0) {size: 0}
new Set({})
// Uncaught TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))

注意: Set 的值比较与 Map 的键比较是一样的,都是基于零值相等算法,即 +0 === -0。

const map = new Map();
const obj = { a: 'a' }
map.set(+0, 1)
console.log(map.get(+0)) // 1
console.log(map.get(-0)) // 1

Set实例属性

  • size: 返回 Set 对象中元素的个数,是只读属性。

Set实例方法

  • add(): 向 Set 对象尾部添加一个元素,返回该 Set 对象。注意: 重复的值会被忽略。

  • clear(): 移除 Set 对象内的所有元素。

  • delete(): 删除 Set 对象中指定的值的元素。如果该元素存在,返回 true,否则返回 false。

    注意: 查找 Set 对象的元素,是基于零值相等算法。

    const set = new Set([1, +0, NaN])
    set.delete(-0)  // true
    set.delete(NaN) // true
    console.log(set) // Set(1) {1}
    
  • has(): 返回一个布尔值,判断给定的值是否存在 Set 对象中。

  • entries(): 返回一个新的迭代对象,是一个包含 Set 对象中所有元素的值的 [value, value] 数组(为与 Map对象 保持相似,每个值的键和值相等),并以插入 Set 对象的顺序排列。

  • values(): 返回一个新的迭代对象,是一个包含 Set 对象中所有元素的值的数组,并以插入 Set 对象的顺序排列。

  • keys(): 与 values() 方法相同。

  • [@@iterator]: 返回一个新的迭代对象,是一个包含 Set 对象中所有元素的值的数组,并以插入 Set 对象的顺序排列。[@@iterator]() 方法与 entries() 方法返回同一个迭代对象。

  • forEach(): 按照插入顺序依次对 Set 中每个值的 [value, value] 对执行一次给定的函数,可以指定第二个参数,在给定的函数内,当 this 使用。与 Array.prototype.forEach() 方法类似。

WeakSet对象

WeakSet 结构与 Set 结构类似,也是一组不同重复值的集合。

WeakSet 与 Set对象的区别有:

  • WeakSet 对象的值必须是对象类型。
  • WeakSet 对象的值是弱引用,也就是说,当键所指对象没有其他地方引用时,它会被垃圾回收机制释放。
  • WeakSet 对象的值是不可枚举的。造成这个问题的原因正是弱引用,其值所对应对象可能会在将来消失,这是不可预知的。

WeakSet 的 API 与 Set 相似,但它只有:add()、delete()、has() 三种方法,没有 size 属性。

相关问题

toPrecision、toFixed、Math.round 有什么区别?

toPrecision() 方法用于处理精度,精度是从左至右第一个不为 0 的数开始数起。

(10.584792).toPrecision(4) // 10.58

toFixed() 方法用于对小数点后指定位数取整,从小数点开始数起。

(10.584792).toFixed(4) // 10.5848

Math.round() 方法用于将一个数字四舍五入到一个整数。

Math.round(10.584792) // 11

什么是纯函数?

纯函数的基本概念:相同的输入,总是会的到相同的输出,并且在执行过程中没有任何副作用。

  • 相同的输入,总是会得到相同的输出:不依赖于程序执行期间函数外部任何状态或数据的变化,必须只依赖于其输入参数。
  • 没有任何副作用:函数在执行过程中产生了外部可观察变化:发起 HTTP 请求、操作 DOM、修改外部数据、输出数据到屏幕或者控制台、 Math.random()、获取的当前时间等。

如何让一个函数调用自身?

在 Javascript 中,一个函数可以用以下三种方法调用自身:

  • 函数名
  • 作用域下的一个指向该函数的变量名
  • arguments.calle (ES5 禁止在严格模式下使用此属性)

调用自身的函数称之为递归函数。在某种意义上说,递归近似于循环。两者都重复执行相同的代码,并且两者都需要一个终止条件(避免无限循环或者无限递归)。

const sum = function loop(a) {
    if (a >= 10) {
        return
    }
    console.log(a)

    // loop(a + 1)
    // sum(a + 1)
    arguments.callee(a + 1)
}
sum(0)

Function 和 eval 创建的函数有什么区别?

eval 和 new Function 都可以用于根据一段字符串创建函数。

Function

Function 构造函数可以创建一个新的 Function 对象。

const sum = new Function('a', 'b', 'return a + b')

Function 构造函数创建的函数,不会创建当前环境的闭包,它们总是被创建于全局环境,因此在运行时它们只能访问全局变量和自己的局部变量,不能访问它们被 Function 构造器创建时所在的作用域的变量。

var x = 10
function func() {
    const x = 1;
    const y = 2;
    const f1 = new Function('var y = 20; console.log(x + y)')
    f1()
}
func()
func() // 30. 作用域是全局作用域和自身的作用域
eval

eval() 函数会将传入的字符串当做 JavaScript 代码进行执行,返回字符串中代码的返回值,如果返回值为空,则返回 undefined。

如果参数不是字符串,则直接返回参数;如果参数字符串表示的是表达式,会对表达式进行求值;如果参数表示一个或多个 JavaScript 语句,则会执行这些语句。

eval(new String('2 + 2')) // String {'2 + 2'}
eval('2*10')              // 20

eval('function sum (a, b) { return a + b }') // 创建一个 sum 函数。
sum(2, 5) // 7

从 ECMAScript 5 起,间接的使用 eval(),它的作用域是全局作用域下,而不是局部作用域中。

var x = 10
var y = 20
function func() {
    const x = 1;
    const y = 2;
    console.log(eval('x + y')) // 3。直接调用,使用词法作用域。

    const indirectEval = eval
    indirectEval('x + y'); // 30。 间接调用,使用全局作用域。

    (0, eval)('x + y')     // 30。 间接调用,使用全局作用域。
}
func()

如果字符串中定义了多个值,则只会返回最后一个值。

var bool = true
const str = "if (bool) { z = 1; } else z = 0;";
console.log(eval(str)); // 1

bool = false
console.log(eval(str)); // 0

eval 返回最后一个表达式的值。

var bool = true;
const str = 'if ( bool ) { 1 + 1; } else { 1 + 2; }';
console.log(eval(str)); // 2

bool = false;
console.log(eval(str)); // 3

函数作为字符串被定义需要 () 作为前缀和后缀。

const str1 = 'function a() {}'
const str2 = '(function a() {})'
console.log(eval(str1)) // undefined
console.log(eval(str2)) // ƒ a() {}
作用域不同

两者的主要区别是作用域不同:eval 中的代码执行时的作用域为自身作用域、当前局部作用域、全局作用域,而 new Function 中的代码执行时的作用域为自身作用域、全局作用域,即不能访问当用局部作用域。

function f1() {
    var x = 20
    return new Function('return x;')
}
function f2() {
    var x = 20
    return eval('(function () { return x })')
}
console.log(f1()()) // Uncaught ReferenceError: x is not defined
console.log(f2()()) // 20

另外,eval() 的执行效率要比 Function() 低。

现代 JavaScript 解释器将 javascript 代码转换为机器代码,这意味着,任何变量命名的概念都会被删除。eval 的使用,会强制浏览器进行冗长的变量名称查找,以确定变量在机器代码中的位置,另外,eval() 解析的字符串可能会声明新变量, 同样也会强制浏览器重新执行所有已经生成的机器代码。

function jsonParse1(obj){
    return eval(`(${obj})`);
}
console.log(jsonParse1("{a: (4-1), b: function(){}, c: new Date()}"))
// {a: 3, c: Sun May 15 2022 01:25:52 GMT+0800 (中国标准时间), b: ƒ}
function jsonParse2(obj) {
    return Function(`"use strict"; return (${obj})`)();
}
console.log(jsonParse2("{a: (4-1), b: function(){}, c: new Date()}"))
// {a: 3, c: Sun May 15 2022 01:26:24 GMT+0800 (中国标准时间), b: ƒ}

这两个函数看似没什么不同,实际执行过程却是不同的:c: new Date() 的执行, 在 Function 函数中,作用域是全局作用域,可以直接访问 window.Date,然而,在 eval 函数中,由于其作用域包括 jsonParse2 的函数作用域,Javascript 引擎必需在 jsonParse2 函数的执行上下文栈查找 Date 对象,因为可能存在如下情况:

function Date() {
    this.name = 'Date'
}
function jsonParse2(obj) {
    return Function(`"use strict"; return (${obj})`)();
}
console.log(jsonParse2("{a: (4-1), b: function(){}, c: new Date()}"))
// {a: 3, c: Date, b: ƒ}

在使用 eval() 的函数时,浏览器被迫进行高代价的查找以检查是否存在名称为 Date 的任何局部变量。与 Function() 相比,这是非常低效的。

参考资料

MDN - JavaScript 数据类型和数据结构

MDN - 使用对象

MDN - 带键的集合

阮一峰 - JavaScript 标准参考教程(alpha)

© lizhao all right reserved,powered by Gitbook文件修订时间: 2022-10-29 21:32:23

results matching ""

    No results matching ""