JavaScript读书笔记
JS的类型
内置类型:string、boolean、number、null 、 undefined、symbol、object
;
除了对象object外,其他统称为“基本类型”
null 有时会被当作一种对象类型,但是这其实只是语言本身的一个 bug,即对 null 执行 typeof null 时会返回字符串 "object"
内置对象
- String
- Number
- Boolean
- Object
- Function
- Array
- Date
- RegExp
Error
- 这些内置函数可以当作构造函数来使用,从而可以构造一个对应子类型的新对象
null
和undefined
没有对应的构造形式,它们只有文字形式。相反,Date 只有构造,没有
使JS对象不可变的方法
- 使用
Object.defineProperty
的属性描述,将writable
和configurable
设置false
- 如果你想禁止一个对象添加新属性并且保留已有属性,可以使用
Object.prevent Extensions(..)
,但可以修改属性的值 Object.seal(..)
会创建一个“密封”的对象,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性,但可以修改属性的值Object.freeze(..)
会创建一个冻结对象,拥有上面的特性,且无法修改它们的值,这个方法是可以应用在对象上的级别最高的不可变性,它会禁止对于对象本身及其任意直接属性的修改
上面四种方法有个共同的缺点:对象属性引用的其他对象是不受影响的,依然可以修改,和const定义数组一样,虽然不能修改数组的指向,但可以向数组push,pop等操作
for..in和Object.keys()区别
Object.keys()
方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和使用for...in
循环遍历该对象时返回的顺序一致。- 两者之间最主要的区别就是
Object.keys()
不会走原型链,而for..in
会走原型链;
对象的属性描述符
Object.defineProperty
什么是闭包
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。
函数的作用域
函数作用域是基于代码的作用域嵌套,而不是调用栈,换句话可以理解为,函数内部的作用域作用域定义在函数定义的地方,如下代码
function foo() {
console.log( a ); // 2
}
function bar() {
var a = 3
foo();
}
var a = 2; bar();
this的指向
this
是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调 用时的各种条件。this
的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
默认绑定
直接使用不带任何修饰的函数引用进行调用的(不包含箭头函数),this
指向全局对象,通常为window
或undefined
(严格模式下)
function foo() {
console.log( this.a );
}
var a = 2; // 实际为window.a = 2
foo(); // 2
隐式绑定
this
指向调用位置的上下文对象,调用引用链很长时,指向调用的最后一层
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
inner: {
a: 3,
foo: foo
}
};
obj.inner.foo(); // 3
此时this是指向foo的调用上下文obj.inner,this.a === obj.inner.a
显式绑定
我们常用的 apply,call,bind
等方法,apply
的第一个是 this
指向的对象,第二参数是数组,传递给调用函数的参数,call
的第一个是 this
指向的对象,后面的所有参数都会传递给要调用的函数,bind
的第一个是 this
指向的对象,之外的其他参数都传给函数进行柯里化
new 绑定
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行原连接。
- 这个新对象会绑定到函数调用的
this
。 - 如果函数没有返回其他对象,那么
new
表达式中的函数调用会自动返回这个新对象。
判断this指向(不含箭头函数)
函数是否在
new
中调用(new
绑定)?如果是的话this
绑定的是新创建的对象。var bar = new foo()
函数是否通过
call、apply
(显式绑定)或者硬绑定调用?如果是的话,this绑定的是 指定的对象。var bar = foo.call(obj2)
函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,
this
绑定的是那个上 下文对象。var bar = obj1.foo()
如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到
undefined
,否则绑定到 全局对象。var bar = foo()
被忽略的this
如果你把 null
或者 undefined
作为 this
的绑定对象传入 call
、apply
或者 bind
,这些值在调用时会被忽略,实际应用的是默认绑定规则,一般使用在函数不关心 this
指向时,仍然需要传入一个占位值,这时 null 可能是一个不错的选择
function foo() {
console.log( this.a );
}
var a = 2;
foo.call( null ); // 2
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 把数组“展开”成参数
foo.apply( null, [2, 3] ); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind( null, 2 ); bar( 3 ); // a:2, b:3
箭头函数的this指向
箭头函数不使用 this
的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this
。
function foo() {
// 返回一个箭头函数
return (a) => {
//this 继承自 foo()
console.log( this.a );
};
}
var obj1 = {
a:2
};
var obj2 = {
a:3
};
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是 3 !
foo() 内部创建的箭头函数会捕获调用时 foo() 的 this。由于 foo() 的 this 绑定到 obj1, bar(引用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。(new 也不行!)
this的指向深入理解
看下面一段代码
let A = {
b: 1,
B: {
b:2,
fun1: function() {
return () => {
console.log(this)
}
},
fun2: () => {
console.log(this)
},
fun3: function () {
console.log(this)
}
}
}
var t1 = A.B.fun1()
A.B.fun1()() // this 指向A.B
t1() // this 指向A.B
var t2 = A.B.fun2
A.B.fun2() // this 指向 window
t2() // this 指向 window
var t3 = A.B.fun3
A.B.fun3() // this 指向A.B
t3() this 指向 window
第一种调用函数返回箭头函数形式,this
应该指向调用 fun1
的调用者,确认之后 this
不会改变,可以看到 A.B
调用 fun1()
生成了箭头函数t1
,此时 this
就确认为了指向 A.B
,且不会改变,A.B.fun1()()
可以看成 (A.B.func1())()
,和上面解释一样
第二种调用,由于 fun2
生成的箭头函数不在一个函数内,因此 this
确认指向全局对象 window
,不管他如何调用
第三种调用就属于咱们常见的普通函数调用,this
指向函数的调用这,即 A.B.fun3()
当然指向 A.B
,t3()
相当于 window.t3()
, 因此 this
指向 window
对象的属性屏蔽
通常情况下,我们认为 在原型链上层已经存在的属性赋值时,就一定会触发屏蔽原型链的相同属性
,虽然在日常开发中,一般是没错的,但如果涉及到原型链上对属性的描述时,就不一定正确了
这时会出现三种情况
- 如果在
[[Prototype]]
链上层存在名为foo的普通数据访问属性(参见第3章)并且没 有被标记为只读(writable:false)
,那就会直接在 myObject 中添加一个名为 foo 的新 属性,它是屏蔽属性。 - 如果在
[[Prototype]]
链上层存在foo,但是它被标记为只读(writable:false)
,那么 无法修改已有属性或者在 myObject 上创建屏蔽属性。如果运行在严格模式下,代码会 抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。 - 如果在
[[Prototype]]
链上层存在foo并且它是一个setter
,那就一定会调用这个 setter。foo 不会被添加到(或者说屏蔽于)myObject,也不会重新定义 foo 这 个setter
。
罪魁祸首是因为赋值运算符 “=”,如果你希望在第二种和第三种情况下也屏蔽 foo,那就不能使用 = 操作符来赋值,而是使用 Object.defineProperty(..)来向 myObject 添加 foo
有时候也会触发隐式屏蔽
var anotherObject = {
a:2
};
var myObject = Object.create( anotherObject );
anotherObject.a; // 2
myObject.a; // 2 使用原型链属性
myObject.a++; // 实际运行 myObject.a = myObject.a + 1 符合上面第一种情况,因此添加个了个屏蔽原型链属性a
myObject.a; // 3
常用的结论和方法
- 对象的
可枚举
就相当于可以出现在对象属性的遍历中
- ES6 中的符号
Symbol.iterator
来获取对象的@@iterator
内部属性 typeof
有一个特殊的安全防范机制,typeof
一个未声明的变量显示undefined- 变量没有类型,但它们持有的值有类型。类型定义了值的行为特征。
delete
运算符可以将单元从数组中删除,单元删除后,数组的 length 属性并不会发生变化toPrecision(..)
方法用来指定有效数位的显示位数:- 最大整数是
2^53 - 1
,即9007199254740991
,最 小 整 数 是-9007199254740991
,如果大于64位需要转化为字符串 - 检测一个值是否是整数,可以使用 ES6 中的
Number.isInteger(..)
方法 - 能使用
==
和===
时就尽量不要使用Object.is(..)
,因为前者效率更高、更为通用。Object.is(..) 主要用来处理那些特殊的相等比较。 Symbol(..)
原生构造函数来自定义符号,不能带new
关键字,否则会出错~~~~
将类数组转化为数组的方法
Array.prototype.slice.call( arguments )
Array.from( arguments )
构造函数constructor的理解
Foo.prototype
的 .constructor
属性只是 Foo
函数在声明时的默认属性。如果你创建了一个新对象并替换了函数默认的 .prototype
对象引用,那么新对象并不会自动获得 .constructor
属性,需要通过原型链查找。当然,你可以给 Foo.prototype
添加一个 .constructor
属性,不过这需要手动添加一个符合正常行为的不可枚举属性。
.constructor
是一个非常不可靠并且不安全的引用,稍不留神 .constructor
就可能会指向你意想不到的地方。
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 创建一个新原型对象
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true
修改Function.prototype
Bar.prototype = Foo.prototype;
缺点:当 你 执 行 类 似 `Bar.prototype. myLabel = ...` 的赋值语句时会直接修改 `Foo.prototype ` 对象本身
Bar.prototype = new Foo();
缺点:调用构造函数可能会想 `this` 添加数据属性
Bar.ptototype = Object.create( Foo.prototype );
ES6 之前需要抛弃默认的 `Bar.prototype`
Object.setPrototypeOf( Bar.prototype, Foo.prototype );
ES6 开始可以直接修改现有的 `Bar.prototype`
instanceof 操作符
a instanceof Foo
在 a 的整条 [[Prototype]]
链中是否有指向 Foo.prototype 的对象
迭代器的理解
ES新增API
Array.of
生成数组Array.from(..)
类数组转化为数组Array.find
在数组中搜索一个值Array.fill(..)
数组填充Array.copyWithin(..)
数组复制Array.findIndex(..)
查找数组一个值得索引- 原型方法 entries()、values()、keys()
Object.is(..)
执行比 === 比较更严格的值比较Object.getOwnPropertySymbols(..)
直接从对象上取得所有的符号属性Object.setPrototypeOf(..)
设置对象原型Object.assign(..)
对象浅拷贝
new.target 能够指向调用 new 的目标构造器
class Parent {
constructor() {
if (new.target === Parent) {
console.log( "Parent instantiated" );
}
else {
console.log( "A child instantiated" );
}
}
}
class Child extends Parent {}
var a = new Parent();
// Parent instantiated
var b = new Child();
// A child instantiated
公开符号Symbol
对于符号的一些属性,笔者只能通过例子来理解
Symbol.iterator
个人理解,通过执行 obj[Symbol.iterator]()
生成一个迭代器来判断 for .. of
的运行,如下
var arr = [4, 5, 6, 7, 8, 9];
for (var v of arr) {
console.log(v);
}
// 4 5 6 7 8 9
// 定义一个只在奇数索引值产生值的迭代器
arr[Symbol.iterator] = function* () {
var idx = 1;
do {
yield this[idx];
} while ((idx += 2) < this.length);
};
for (var v of arr) {
console.log(v);
}
// 5 7 9
第一步执行 for (var v of arr)
执行了 arr[Symbol.iterator]
生成一个迭代器,通过调用this.next()
var list = {
a: 1,
[Symbol.iterator]() {
return this
},
next() {
if (this.a < 5) {
this.a++
return {
value: this.a,
done: true
}
}
return {
value: this.a,
done: false
}
}
}
var i = 0
for (var item of list) {
console.log(item)
if (i > 5) break;
i++
}
// 2 3 4 5
个人理解
通过上面比较可以认为一个对象只要通过 [Symbol.iterator]
生成一个符合要求的迭代器,对应第一种,或者一个对象有 [Symbol.iterator]
和 next
对应第二种,就可以调用 for ... of
进行迭代
Symbol.toStringTag 与 Symbol.hasInstance
Symbol.toStringTag
相当于改变 Object.prototype.toString
得到的结果 [object xxx]
中的 xxx
Symbol.hasInstance
修改子例是否是实例行为特性
function Foo(greeting) {
this.greeting = greeting;
}
Foo.prototype[Symbol.toStringTag] = "Foo";
Object.defineProperty( Foo, Symbol.hasInstance, {
value: function(inst) {
return inst.greeting == "hello";
}
} );
var a = new Foo( "hello" ),
b = new Foo( "world" );
b[Symbol.toStringTag] = "cool";
a.toString(); // [object Foo]
String( b ); // [object cool]
a instanceof Foo; // true
b instanceof Foo; // false
Symbol.toPrimitive
任意对象值上作为属性的符号 @@toPrimitivesymbol 都可以通过指定一个 方法来定制这个 ToPrimitive 强制转换
var arr = [1, 2, 3, 4, 5];
arr + 10; // 1,2,3,4,510
arr[Symbol.toPrimitive] = function (hint) {
if (hint == "default" || hint == "number") {
// 求所有数字之和
return this.reduce(function (acc, curr) {
return acc + curr;
}, 0);
}
};
arr + 10; // 25