JS浅拷贝和深拷贝

值和引用

简单值(即标量基本类型值)总是通过值复制的方式来赋值/传递,包括null、undefined、字符串、数字、布尔和ES6中的symbol。

复合值–对象(包括数组和封装对象)和函数,则总是通过引用复制的方式来赋值/传递。

var a = 2;
var b = a; // b是a的值得一个复本
b++;
a; // 2
b; // 3

var c = [1,2,3];
var d = c; // d是[1,2,3]的一个引用
d.push(4);
c; // [1,2,3,4]
d; // [1,2,3,4]

变量a是一个标量基本类型值,持有该值得一个复本,b持有它的另一个复本。b更改时,a的值保持不变。

c和d则分别指向同一个复合值[1,2,3]的两个不同引用。请注意,c和d仅仅是指向值[1,2,3],并非持有。所以它们更改的是同一个值,随后它们都指向更改后的新值[1,2,3,4]。

由于引用指向的是值本身而非变量,所以一个引用无法更改另一个引用的指向。如下例,b=[4,5,6]并不影响a指向值[1,2,3],除非b不是指向数组的引用,而是指向a的指针,但在js中不存在这样的困惑:

var a = [1,2,3];
var b = a;
a; // [1,2,3]
b; // [1,2,3]

b = [4,5,6];
a; // [1,2,3]
b; // [4,5,6]

浅拷贝和深拷贝

在js中,对于Object和Array这类引用类型值,当从一个变量向另一个变量复制引用类型值时,这个值的副本其实是一个指针,两个变量指向同一个堆对象,改变其中一个变量,另一个也会受到影响。这种拷贝分为两种情况:拷贝引用和拷贝实例,也就是我们说的浅拷贝和深拷贝。

浅拷贝

拷贝原对象的引用,这是最简单的浅拷贝。除了上述的方法,还有:

1.Object.assign()

Object.assign()方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。

var x = {
    a: 1,
    b: { f: {g: 1} },
    c: [1,2,3]
};
var y = Object.assign({}, x);
console.log(y.b.f === x.b.f); // true

深拷贝

深拷贝也就是拷贝一个新的实例,新的实例和之前的实例互不影响。主要方法有:

1.Array的slice和concat方法

Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。之所以把它放在深拷贝里,是因为它看起来像是深拷贝。而实际上它是浅拷贝。原数组的元素会按照下述规则拷贝:

  • 如果该元素是个对象引用(不是实际的对象),slice会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。
  • 对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。
var o1 = ['darko', {age: 22}];
var o2 = o1.slice(); // 根据Array.prototype.slice()的特性,这里会返回一个o1的浅拷贝对象

console.log(o1 === o2); // => false,说明o2拷贝的是o1的一个实例

o2[0] = 'lin';
console.log(o1[0]); // => "darko" o1和o2内部包含的基本类型值,复制的是其实例,不会相互影响

o2[1].age = 23;
console.log(o1[1].age); // =>23 o1和o2内部包含的引用类型值,复制的是其引用,会相互影响

2.JSON对象的parse和stringfy

JSON对象parse方法可以将JSON字符串反序列化成JS对象,stringfy方法可以将JS对象序列化成JSON字符串,借助这个方法,也可以实现对象的深拷贝。

var source = { name: 'source', child: { name: 'child' } };
var target = JSON.parse(JSON.stringfy(source));
target.name = 'target'; // 改变target的name属性
console.log(source.name); // source
console.log(target.name); // target
target.child.name = 'target child'; // 改变target的child
console.log(source.child.name); // child
console.log(target.child.name); // target child

null和undefined详解

区别

当声明的变量还未被初始化时,变量的默认值为undefined。
null用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象。

undefined == null // true
undefined === null // false

null是一个表示‘无’的对象,转为数值时为0;undefined是一个表示‘无’的原始值,转为数值时为NaN。

Number(null) // 0
5 + null // 5

Number(undefined) // NaN
5 + undefined // NaN

undefined表示‘缺少值’,就是此处应该有一个值,但是还没有定义。典型用法是:

  1. 变量被声明了,但还没有赋值时,就等于undefined。
  2. 调用函数时,应该提供的参数没有提供,该参数等于undefined。
  3. 对象没有赋值的属性,该属性的值为undefined。
  4. 函数没有返回值时,默认返回undefined。

null表示‘没有对象’,即该处不应该有值。典型用法是:

  1. 作为函数的参数,表示该函数的参数不是对象。
  2. 作为对象原型链的终点。

关于赋值

undefined可以被赋值,而null不可以被赋值:

  1. undefined、NaN和Infinity都是全局对象window的属性。既然是属性,当然可以赋值。然而这三个属性又是不可写的属性,即它们的内部特性[[writable]]为false,所以赋值无效。
  2. null是一个字面量,准确地说叫做Null字面量。与true和false类似。它们都属于js的保留字,换句话说它们都是值,与数字值123、字符串值’fttbar’一样,当然不能被赋值了。

特点

可以给undefined赋值,但是注意不要把它放到全局作用域:

function foo() {
    var undefined = 10;
    console.log(undefined);
}
foo(); // 10

function foo() {
    undefined = 10;
    console.log(undefined);
}
foo(); // undefined

通过Object.getOwnPropertyDescriptor方法,可以证明undefined是window对象的只读属性:

Object.getOwnPropertyDescriptor(window, 'undefined');

/** 输出:Object {value: undefined, writable: false,
enumerable: false, configurable: false} **/

在严格模式下,给undefined赋值会报错。因为严格模式下,禁止给对象的只读属性赋值:

function foo() {
    'use strict';
    undefined = 10;
    console.log(undefined); // TypeError
}
foo();

undefined和null的类型:

typeof undefined // 'undefined'
typeof null // 'object'

// 需要使用复合条件来检测null值得类型
var a = null;
(!a && typeof a === 'object'); // true

数组和类数组

数组

在js中,数组可以容纳任何类型的值,可以是字符串、数字、对象,甚至是其他数组(多维数组就是通过这种方式来实现的):

var a = [1, '2', [3]];
a.length; // 3

对数组声明后即可向其中加入值,不需要预先设定大小:

var a = [];
a.length; // 0

a[0] = 1;
a[1] = '2';
a[2] = [3];
a.length; // 3

使用delete运算符可以将单元从数组中删除,但是删除后,数组的length属性并不会发生变化。

创建稀疏数组,空白单元为undefined,但仍占有length;字符串键值不占有length;字符串键值能够被强制转换为十进制数字的话,会被当作数字索引来处理:

var a = [];
a[0] = 1;
a['2'] = 2; // 稀疏数组,仍存在a[1];该字符串键值可以转换为数字,当作a[2]处理
a['foo'] = 3; // 该字符串键值不能转为数字,不占有length

a.length; // 3
a[0] === 1 
a[1] === undefined 
a[2] === 2

类数组

有时需要将类数组(一组通过数字索引的值)转换为真正的数组,这一般通过数组工具函数(如indexof(..)、concat(..)、forEach(..)等)实现。

一些DOM查询操作会返回DOM元素列表,它们并非真正意义上的数组,但十分类似;通过arguments对象(类数组)将函数的参数当作列表来访问(从ES6开始已废止)。工具函数slice(..)经常被用于这类转换:

function foo() {
    var arr = Array.prototype.slice.call(arguments);
    arr.push('bam');
    console.log(arr);
}
foo('bar', 'baz'); // ['bar', 'baz', 'bam']

ES6中的内置工具函数Array.from(..)也能实现同样的功能:

var arr = Array.from(arguments);

类数组详解

定义

拥有length属性,其他属性(索引)为非负整数(对象中的索引会被当做字符串来处理,这里你可以当做是个非负整数串来理解);不具有数组所具有的方法。实际上,只要有length属性,且它的属性值为number类型就行。

类数组示例:

var a = {'1':'gg','2':'love','4':'meimei',length:5};
Array.prototype.join.call(a,'+'); // '+gg+love+meimei'

非类数组示例(没有length属性,所以不是类数组):

var c = {'1':2};

js中常见的类数组有arguments对象和DOM方法的返回结果。

类数组判断

function isArrayLike(o) {
    if (o &&                                // o is not null, undefined, etc.
        typeof o === 'object' &&            // o is an object
        isFinite(o.length) &&               // o.length is a finite number
        o.length >= 0 &&                    // o.length is non-negative
        o.length===Math.floor(o.length) &&  // o.length is an integer
        o.length < 4294967296)              // o.length < 2^32
        return true;                        // Then o is array-like
    else
        return false;                       // Otherwise it is not
}

类数组表现

之所以称为‘类数组’,就是因为和‘数组’类似。不能直接使用数组方法,但你可以像使用数组那样,使用类数组。

var a = {'0':'a', '1':'b', '2':'c', length:3};  // An array-like object
Array.prototype.join.call(a, '+'');  // => 'a+b+c'
Array.prototype.slice.call(a, 0);   // => ['a','b','c']
Array.prototype.map.call(a, function(x) { 
    return x.toUpperCase();
});                                 // => ['A','B','C']

类数组对象转化为数组

有时候处理类数组对象的最好方法是将其转化为数组,然后就可以直接使用数组方法啦。

Array.prototype.slice.call(arguments)

var a = {'0':1,'1':2,'2':3,length:3};
var arr = Array.prototype.slice.call(a); // arr === [1,2,3]

Array方法之fill、filter、every、some、map、reduce

fill方法

使用指定值填充数组。

语法

array.fill(value [ , start [ , end]])

参数

array:必需。数组对象。
value:必需。用于填充数组的值。
start:可选。用于填充数组值的起始索引。默认值为0。
end:可选。用于填充数组值的结束索引。默认值是this对象的length属性。

备注

如果start为负,则start被视为length+start,其中,length是数组的长度。如果end为负,则end被视为length+end。

实例

[0, 0, 0].fill(7, 1) // [0, 7, 7]
[0, 0, 0].fill(7) // [7, 7, 7]

filter方法

返回数组中的满足回调函数中指定的条件的元素。

语法

array.filter(callback[, thisArg])

参数

array:必需。一个数组对象。
callback:必需。一个接受最多三个参数的函数。对于数组中的每个元素,filter方法都会调用callback函数一次。
thisArg:可选。可在callback函数中为其引用this关键字的对象。如果省略thisArg,则undefined将用作this值。

返回值

一个包含回调函数为其返回true的所有值得新数组。如果回调函数为array的所有元素返回false,则新数组的长度为0。

备注

对于数组中的每个元素,filter方法都会调用callback函数一次(采用升序索引顺序)。不为数组中缺少的元素调用该回调函数。除了数组对象外,filter方法可由具有length属性且具有已按数字编制索引的属性名的任何对象使用。

实例

不带第三个参数

var arr = [5, 'element', 10, 'the', true]

var result = arr.filter(
    function (value) {
        return (typeof value === 'string')
    }
)

console.log(result) // element, the

带第三个参数

var numbers = [6, 12, '15', 16, 'the', -12]

var checkNumericRange = function (value) {
    if (typeof value !== 'number')
        return false
    else
        return value >= this.mininum && value <= this.maxinum
}

var obj = { mininum: 10, maxinum: 20 }

var result = numbers.filter(checkNumericRange, obj)

console.log(result) // 12, 16

every方法

语法

array.every(callback[, thisArg])

参数

array:必需。一个数组对象。
callback:必需。一个接受最多三个参数的函数。every方法会为array中的每个元素调用callback函数,直到callback返回false,或直到到达数组的结尾。
thisArg:可选。可在callback函数中为其引用this关键字的对象。如果省略thisArg,则undefined将用作this值。

返回值

如果callback函数为所有数组元素返回true,则为true;否则为false。如果数组没有元素,则every方法将返回true。

备注

every方法会按升序顺序对每个数组元素调用一次callback函数,直到callback函数返回false。如果找到导致callback返回false的元素,则every方法会立即返回false。否则,every方法返回true。

实例

不带第三个参数

var numbers = [2, 4, 5, 6, 8]

function CheckIfEven(value, index, ar) {
    console.log(value + '')
    if (value % 2 === 0)
        return true
    else
        return false
}

if (numbers.every(CheckIfEven))
    console.log('All are even')
else
    console.log('Some are not even')

// 2 4 5 Some are not even

带第三个参数

var numbers = [10, 15, 19]

var checkNumericRange = function(value) {
    if (typeof value !== 'number')
        return false
    else
        return value >= this.mininum && value <= this.maxinum
}

var obj = { mininum: 10, maxinum: 20 }

if (numbers.every(checkNumericRange, obj))
    console.log('All are within range')
else 
    console.log('Some are not within range')

// All are within range

some方法

语法

array.some(callback[, thisArg])

参数

array:必需。一个数组对象。
callback:必需。一个接受最多三个参数的函数。some方法会为array中的每个元素调用callback函数,直到callback返回true,或直到到达数组的结尾
thisArg:可选。可在callback函数中为其引用this关键字的对象。如果省略thisArg,则undefined将用作this值。

返回值

如果callback函数为任何数组元素均返回true,则为true;否则为false

备注

some方法会按升序索引顺序对每个数组元素调用callback函数,直到callback函数返回true。如果找到导致callback返回true的元素,则some方法会立即返回true。如果回调不对任何元素返回true,则some方法会返回false。

实例

不带第三个参数

var numbers = [1, 15, 4, 10, 11, 22]

function CheckIfEven(value, index, ar) {
    if (value % 2 === 0)
        return true
}

var evens = numbers.some(CheckIfEven)

console.log(evens)
// true

带第三个参数

var numbers = [6, 12, 16, 22, -12]

var isOutsideRange = function (value) {
    return value < this.mininum || value > this.maxinum
}

var range = { mininum: 10, maxinum: 20 }

console.log(numbers.some(isOutsideRange, range))
// true

原型和原型链

构造函数

使用构造函数来创建对象

function Person() {

}
var person1 = new Person()

Person就是一个构造函数,通过new创建了person1对象实例。其实构造函数就和普通函数没有多大区别,首字母大写只是约定俗成,不大写照样可以。关键是调用它的方式——通过new,那么这里又会牵扯到另一个问题,使用new调用后会内部会执行哪些操作。

prototype

function Person() {

}
Person.prototype.name = 'lin'
var person1 = new Person()
console.log(person1.name) // lin

这个并不是构造函数专有,每个函数都会有一个prototype属性,这个属性是一个指针,指向一个对象,记住只有函数才有,并且通过bind()绑定的也没有。prototype指向Person.prototype,Person.prototype就是原型对象,也就是person1的原型。原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。

proto

function Person() {

}
Person.prototype.name = 'lin'
var person1 = new Person()
console.log(person1.__proto__ === Person.prototype) // true

所以总结可得proto就是用来将对象与该对象的原型相连。

// instanceof这个操作符只能处理对象(person1)和函数(带.prototype引用的Person)之间的关系
person1 instanceof Person // true

// isPrototypeOf如果[[prototype]]指向调用此方法的对象,那么这个方法就会返回true
Person.prototype.isPrototypeOf(person1) // true

// Object.getPrototypeOf这个方法返回[[prototype]]的值,可以获取到一个对象的原型
Object.getPrototypeOf(person1) === Person.prototype // true

constuctor

function Person() {

}
var person1 = new Person()
Person.prototype.constructor === Person // true
person1.constructor === Person // true

原型链

我们既然探索完了他们的关系,那我们来继续探索一下原型和原型链的奥秘所在,其实原型链就是依托proto和prototype连接起来的。
下面代码中在实例属性和原型属性都有一个名为name的属性,但是最后输出来的是实例属性上的值。当我们读取一个属性的时候,如果在实例属性上找到了,就读取它,不会管原型属性上是否还有相同的属性,这其实就是属性屏蔽。即当实例属性和原型属性拥有相同名字的时候,实例属性会屏蔽原型属性,记住只是屏蔽,不会修改,原型属性那个值还在。

function Person() {

}
// 原型属性
Person.prototype.name = 'lin'

var person1 = new Person()

// 实例属性
person1.name = 'L'

console.log(person1.name) // L

下面代码中person1实例并没有name属性,但仍然可以输出值,就是在原型上找到的。

function Person() {

}
// 原型属性
Person.prototype.name = 'lin'

var person1 = new Person()

console.log(person1.name) // lin

使用方法hasOwnProperty,属性只有存在于实例中才会返回true

function Person() {

}
var person1 = new Person()

// 实例属性
person1.name = 'L'
person1.hasOwnProperty('name') // true

in操作符则会遍历所有属性,不管是实例上的,还是原型上的。

'name' in person1 // true

Object.prototype没有原型,为null,它就是尽头。

Object.prototype.__proto__ // null

隐式类型转换的方式

数字运算符

使用数字运算符“+”或“-”:

10 + '5' = '105'  +转换为字符串
10 - '5' = 5  -转换为数字

.调用方法

‘aaa’为直接类型,不含方法;new String(‘aaa’)为对象类型,含原型方法。’aaa’.split(‘’)时实际将直接类型转换为对象类型来操作,也是一种隐式类型转换。

if语句

== 或 ===

JS中的一元运算符

delete

delete运算符删除对以前定义的对象属性或方法的引用,例如:

var person = new Object();
person.name = 'www';
document.write(person.name); // www
delete person.name;
document.write(person.name); // undefined

void

void运算符对任何值都返回undefined,该操作符通常用于避免输出不该输出的值。

前增量/前减量运算符

var i = 100;
var j = 10;
document.write(++i + ++j); // 112

后增量/后减量运算符

var i = 100;
var j = 10;
document.write(i++ + j++ + "<br/>"); // 110
document.write(i++ + j++ + "<br/>"); // 112

一元加法

一元加法可以将字符串转换为数字:

var str = '100';
document.write(typeof(str)+'<br/>'); // string
var num = +str;
document.write(typeof(num)); // number

Object.defineProperty用法

三个参数(均为必填)

  1. obj:需要定义属性的对象(目标对象)
  2. prop:需要被定义或修改的属性名(对象上的属性或者方法)
  3. descriptor:需被定义或修改的属性的描述符
// Object.defineProperty(obj, prop, descriptor)
var obj = {};
Object.defineProperty(obj, 'a', {
    value: 2,
    writable: true,
    configurable: true,
    enumerable: true
});
obj.a; // 2

descriptor参数详解

value:属性的值

var obj = {};
Object.defineProperty(obj, 'a', {
    value: 2
})
console.log(obj.a); // 2

writable:属性的值是否能被修改

var obj = {};
Object.defineProperty(obj, 'a', {
    value: 2,
    writable: false, // 不可写!
    configurable: true,
    enumerable: true
})
console.log(obj.a); // 2
obj.a = 3;
console.log(obj.a); // 2

configurable:是否能够配置value, writable, enumerable

var obj = {};
Object.defineProperty(obj, 'a', {
    value: 2,
    writable: true,
    configurable: false, // 不可配置!
    enumerable: true
})
console.log(obj.a); // 2
Object.defineProperty(obj, 'a', {
    value: 3,
    writable: true,
    configurable: true,
    enumerable: true
})
// TypeError: Cannot redefine property: a

enumerable:属性是否能在for…in或者Object.keys中被枚举出来

var obj = {};
Object.defineProperty(obj, 'a', {
    value: 2,
    writable: true,
    configurable: true,
    enumerable: false // 不可枚举!
})
console.log(Object.keys(obj)); // []
Object.defineProperty(obj, 'a', {
    value: 2,
    writable: true,
    configurable: true,
    enumerable: true
})
console.log(Object.keys(obj)); // ['a']

get/set

对于set和get,我的理解是它们是一对勾子函数,当你对一个对象的某个属性赋值时,则会自动调用相应的set函数;而当获取属性时,则调用get函数。这也是实现双向数据绑定的关键。

var obj = {}
var a

Object.defineProperty(obj, 'a', {
    get: function() {
        console.log('get x')

        // 我们可以在这里对返回的值做任何操作
        return x + 1
    },
    set: function(newValue) {
        console.log('set x to', newValue)
        x = newValue
    }
})

obj.a = 100

console.log(obj.a);

/*
output:
  set x to 100
  get x
  101
*/

注意

数据描述符和存取描述符不能混合使用

Object.defineProperty(o, "conflict", {
  // value是数据描述符
  value: 1,
  // get是存取描述符
  get: function() {
    return 2;
  }
});

视图和数据变化绑定

<div>
    <p>你好,<span id='nickName'></span></p>
    <div id="introduce"></div>
</div>
//视图控制器
var userInfo = {};
Object.defineProperty(userInfo, "nickName", {
    get: function(){
        return document.getElementById('nickName').innerHTML;
    },
    set: function(nick){
        document.getElementById('nickName').innerHTML = nick;
    }
});
Object.defineProperty(userInfo, "introduce", {
    get: function(){
        return document.getElementById('introduce').innerHTML;
    },
    set: function(introduce){
        document.getElementById('introduce').innerHTML = introduce;
    }
})
userInfo.nickName = "xxx";
userInfo.introduce = "我是xxx,我来自云南,..."

vue数据变动

但是,这个例子只是数据和dom节点的绑定,而vue.js更为复杂一点,它在网页dom和accessor之间会有两层,一层是Wacher,一层是Directive,比如以下代码。

var a = { b: 1 }
var vm = new Vue({ 
  data: data
})

把一个普通对象(a={b:1})传给 Vue 实例作为它的 data 选项,Vue.js 将遍历它的属性,用Object.defineProperty 将它们转为 getter/setter,如图绿色的部分所示。
每次用户更改data里的数据的时候,比如a.b =1,setter就会重新通知Watcher进行变动,Watcher再通知Directive对dom节点进行更改。

Array方法之slice和splice

slice

slice是指定在一个数组中的元素创建一个新的数组,即原数组不会变

var color = new Array('red', 'blue', 'yellow', 'black');
var color2 = color.slice(1,2);
alert(color); //输出 red,blue,yellow,black
alert(color2); //输出 blue;注意:这里只有第二项一个值

splice

splice是js中数组功能最强大的方法,它能够实现对数组元素的删除、插入、替换操作,返回值为被操作的值。

splice删除:color.splice(1,2)(删除color中的1、2两项);

splice插入:color.splice(1,0,’brown’,’pink’)(在color键值为1的元素前插入两个值)

splice替换:color.splice(1,2,’brown’,’pink’)(在color中替换1、2元素)

var color = new Array('red','blue','yellow','black');
var color2 = color.splice(2,3,'brown','pink');
alert(color);     //  red,blue,brown,pink
alert(color2);    //  yellow,black

JS中的三种遍历方法:forEach,for...in,for...of

forEach方法

1、基本用法

相比于传统的JS遍历方法简单了许多,但是不足处在于不能中断循环,不能使用break语句或使用return语句。

['a', 'b', 'c'].forEach((v) => {
    console.log(v);
})
// 输出'a', 'b', 'c'

for…in方法

1、基本用法

简单来说,for…in是用来遍历键名的方法。

var k = ['a', 'b', 'c'];
for (i in k) {
    console.log(i);
}
// 输出为'0','1','2',且为字符串

2、可以遍历字符串(同for…of方法)

var k = 'abc';
for (i in k) {
    console.log(k[i]);
}
// 输出为'a','b','c'

3、可以使用break终止(同for…of方法)

var k = [10,20,30];
for (i in k) {
    console.log(k[i]);
    if (k[i]>15) {break;}
}
// 输出为10, 20

4、可以遍历对象

var k = {
  'a': 'x',
  'b': 'y',
  'c': 'z'
};
for (i in k) {
    console.log(k[i]);
}
// 输出为'x', 'y', 'z'

for…of方法

1、基本用法

简单来说,for…of是用来遍历键值的方法。

var k = ['a', 'b', 'c'];
for (v of k) {
    console.log(v);
}
// 输出为'a','b','c'

2、可以遍历字符串(同for…in方法)

var k = 'abc';
for (v of k) {
    console.log(v);
}
// 输出为'a','b','c'

3、可以使用break终止(同for…in方法)

var k = [10,20,30];
for (v of k) {
    console.log(v);
    if (v>15) {break;}
}
// 输出为10, 20

4、可以支持Map和Set对象遍历

var k = new Map([['a', 1], ['b', 2], ['c', 3]]);
for ([i, v] of k) {
    console.log(v);
}
// 输出为1, 2, 3
var k = new Set([1,1,2,2,3,3]);
for (v of k) {
    console.log(v);
}
// 输出为1,2,3