第一部分
第二章 词法作用域
作用域逐级查找的规则,会产生“遮蔽效应”,也就是内部的标识符遮蔽了外部的标识符。
但是通过window.a
可以访问那些被同名变量所遮蔽的全局变量。但非全局的变量如果被遮蔽了,无论如何都无法被访问到。
词法作用域的意义在于,无论函数在哪里被调用,无论如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。
词法作用域查找只会查找一级标识符。比如a,b,c这种。如果代码中引用了foo.bar.baz,词法作用域只会去查找foo标识符,找到后使用对象属性访问规则来接管对bar和baz的访问。
修改词法作用域:
- eval
eval中执行的代码可能会通过声明新变量等方式修改词法作用域。
但是在严格模式中,eval在运行时有自己的词法作用域,意味着其之中的声明无法修改所在的作用域。
- with
with本来是用于重复引用同一个对象中的多个属性的快捷方式:
var o = {a:1,b:2,c:3}
with(o){
a =3;
b=4;
c=5;
}
with其实是根据传递进去的对象创建了一个全新的词法作用域。
因此当我们给一个并不存在的属性赋值时:
with(o){
d:6;
}
o中并没有d标识符,因此会遵循作用域查找规则向上查找,而直到全局作用域都没有找到,就会自动创建一个全局变量(非严格模式)。因此最终d会被泄漏到全局中去,而没有达到给o添加属性的目的。
在严格模式中,with完全被禁止了。
eval和with在运行时修改或者创建新的作用域,非常影响性能。
因为js引擎会在编译阶段进行一系列性能优化,其中一些优化依赖于根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。
但是遇到了eval和with,可能之前的优化都没有意义,因为无法知道传递给它们的会如何修改作用域,引擎就可能完全不做优化。
第三章 函数作用域和块作用域
区分函数表达式与函数声明:如果function是声明中的第一个词,那么就是一个函数声明,否则是一个函数表达式。
函数表达式可以是匿名的,而函数声明则不可以省略函数名。
let循环
for(let i=0;i<10;i++){
console.log(i);
}
console.log(i);//ReferrenceError
let在循环中使用不仅将i绑定到所在的块级作用域中,并且它在每一次迭代中都会将上次迭代结束的值重新进行绑定,并重新赋值。
第四章 提升
在编译阶段,包括变量和函数在内的所有声明都会被首先处理,也就是提升。而赋值或其他运行逻辑会留在原地等待执行阶段。
注意,每个作用域内都会发生提升,而不只是全局作用域。函数作用域内也会进行提升。
函数表达式不会被提升,这是它与函数声明最大的区别。
函数首先被提升,然后是变量。
并且当变量声明重复时,后面的声明会被忽略:
foo();
var foo;
function foo(){
console.log(1);
}
foo = function(){
console.log(2);
}
会被引擎理解为如下形式:
function foo(){
console.log(1);
}
foo();
foo = function(){
console.log(2);
}
由于函数声明会被提升到普通变量之前,因此var foo在foo函数声明之后是重复的声明,就被忽略了。
尽管重复的var声明会被忽略,但是后面的函数声明还是可以覆盖前面的函数声明:
foo();
function foo(){
console.log(1);
}
var foo = function(){
console.log(2);
}
function foo(){
console.log(3);
}
最后会输出3.
第五章 作用域闭包
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
第二部分
第一章 this
引用函数自身的方式:
- 针对具名函数,可以使用函数名来引用此函数:
function foo(){
foo.count = 4;
}
- 而对于匿名函数,由于没有名称标识符,只能使用不推荐的arguments.callee
this是在运行时绑定的,它的值取决于函数调用的方式。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当函数被调用时,会创建一个执行上下文,this是执行上下文中的一个属性。
第二章 this
判断this的值时,对象属性引用链中只有最后一层在调用位置中起作用:
function foo(){
console.log(this.a);
}
var obj2 = {
a:42,
foo:foo
}
var obj1 = {
a:2,
obj2:obj2
}
obj1.obj2.foo(); //42
赋值会造成this默认绑定的丢失:
var a = "global";
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
}
var bar = obj.foo;
bar(); //global
bar引用的是foo函数本身,和obj没有任何关系了。
传入回调函数时也是相同的:
var a = "global";
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
}
function doFun(fn){
fn();
}
doFun(obj.foo); //global
使用new调用函数,会执行下面的操作
- 创建一个新对象
- 将新对象的原型对象设为该函数的prototype值
- 将新对象绑定到函数内部的this上
- 如果函数没有返回其他对象,那么就返回这个新对象。
如果给call、apply、bind传入的是null或者undefined,那么这些值会在调用时被忽略,实际采用的是默认绑定规则,也就是this指向window或undefined。
比如说使用apply来展开一个数组:
function foo(a,b){
return a+b;
}
foo.apply(null,[2,3])
当然在ES6中,可以直接使用…来展开数组:foo(...[2,3])
Object.create(null)
可以创建一个空对象,比{}
还空,因为并不会创建Object.prototype
这个委托。
当我们不想指定call、apply、bind中的this时,就可以使用这样一个空对象,以免泄漏到全局中。
第三章 对象
一些内置对象:Array、Function、Date、RegExp、Error
对象中的属性名永远是字符串,如果你使用string以外的其他值作为属性名,那么它首先会被转换成一个字符串。数字类型的也不例外,要与数组中数字用法区分开。
var obj = {};
obj[true] = "foo";
obj[3] = "bar";
obj[obj] = "baz"
console.log(obj);
//{"3": "bar", "true": "foo", "[object Object]": "baz"}
可以看到所有属性名都被转换为了字符串,尤其是最后一个,将对象转换为了字符串。
数组也可以添加属性,但是数组的长度却并不会发生变化:
var myArr = [];
myArr.baz = "baz";
myArr.length; //0
但是如果添加的属性能够被转换为数字的话,就会变成一个数值下标,从而修改数组的内容而不是添加一个属性。
myArr["3"] = "baz";
myArr.length; //4
对象复制:
- 对于可被JSON序列化的对象来说,可以使用JSON的方法进行深拷贝:
var copyObj = JSON.parse(JSON.stringify(obj));
- Object.assign
ES6新定义的方法,实现浅拷贝,
Object.assign(target, ...sources)
第一个参数是目标对象,之后是一个或多个源对象。它会遍历一个或多个源对象的所有可枚举的自有键,并把它复制到目标对象上,最后返回目标对象。
属性描述符:
Object.getOwnPropertyDescriptor(obj, prop)
返回指定对象上一个自有属性的属性描述符。
var obj = {a:2};
Object.getOwnPropertyDescriptor(obj,"a");
//{
// configurable:true,
// enumerable:true,
// value:2,
// writable:true
//}
在创建普通属性时属性描述符会使用默认值,就像上面的a属性一样。我们也可以使用Object.defineProperty()
来添加一个新属性或者修改一个已有属性(如果它是configurable的)并对特性进行设置:
var obj = {};
Object.defineProperty(obj,"a",{
value:2,
configurable:true,
enumerable:true,
writable:true
})
下面来介绍一下这些属性的特性:
- Writable
是否可以修改属性的值。
var obj = {};
Object.defineProperty(obj,"a",{
value:2,
configurable:true,
enumerable:true,
writable:false
})
obj.a = 3;
obj.a; //2
在严格模式下,尝试为writable为false 的属性赋值会报TypeError错。
- Configurable
属性是否可配置。若可配置,就可以使用defineProperty方法来修改描述符。
var obj = {};
Object.defineProperty(obj,"a",{
value:2,
configurable:false, //不可配置
enumerable:true,
writable:true
})
obj.a = 5;
obj.a; //5
Object.defineProperty(obj,"a",{
value:2,
configurable:true,
enumerable:true,
writable:true
})
最后一个defineProperty试图重新将此属性设置为可配置的,但是会产生一个TypeError错误。
不管是不是严格模式,尝试修改一个不可配置的属性描述符都会出错。 把configurable设为false是一个单向操作。
除了无法修改,configurable:false还会禁止删除这个属性。
Object.defineProperty(obj,"a",{
value:2,
configurable:false, //不可删除
enumerable:true,
writable:true
})
delete obj.a;
obj.a; //2
可见删除失败了。
- Enumerable
此属性是否可枚举。
可枚举的属性会出现在for-in循环中。
不变性
常量属性:不可修改、重定义或者删除
- configurable:false
- writable:false
禁止扩展:禁止一个对象添加新属性并保留已有属性。
Object.preventExtensions(obj)
var obj = {a:2};
Object.preventExtensions(obj);
obj.b = 3;
obj.b; //undefined
在严格模式下会抛出TypeError错误。
密封:
Object.seal()
会创建一个密封的对象,也就是不仅不能添加新属性,也不能重新配置或者删除现有属性。
其实这个方法会调用Object.preventExtensions(obj)
并将现有属性标记为configurable:false冻结:
Object.freeze()
会创建一个冻结对象,其实是调用Object.seal()
并将所有属性标记为writable:false。
如此一来这个对象属性的值也都不能更改。
访问描述符:
可以使用getter和setter改写某个属性在读写时的默认操作。
getter是一个隐藏函数,会在获取属性值时调用;
setter是一个隐藏函数,会在设置属性值时调用。
当你给一个属性定义getter、setter时,这个属性会被定义为“访问描述符”。
对于访问描述符,js引擎会忽略它们的value和writable特性。
var obj = {
get a(){
return 2;
}
}
Object.defineProperty(obj,"b",{
get:function(){
return this.a*2;
},
enumerable:true
})
obj.a; //2
obj.b; //4
上面两种方法都可以定义getter,二者都会在对象中创建一个不包含值的属性,对这个属性的访问会自动调用一个隐藏函数,它的返回值会被当作属性访问的返回值。
obj.a = 3;
obj.a; //2
由于我们只定义了a的getter,所以对a的值进行设置时会忽略赋值操作。而且即使有合法的setter,由于我们自定义的getter只会返回2,因此set操作是无意义的。
因此为了让属性更合理,需要定义setter。
var obj = {
get a(){
return this._a;
},
set a(val){
this._a = val*2;
}
}
obj.a = 2;
obj.a; //4
属性的存在性
in操作符 会检查属性是否在对象及其原型链中
hasOwnProperty则只会检查属性是否在对象中,不会检查原型链。
Object.keys()和Object.getOwnPropertyNames()都只会查找对象自有的属性。
Object.keys()返回一个数组,包含所有可枚举属性。
Object.getOwnPropertyNames()返回一个数组,包含所有属性,无论是否可枚举
有关枚举
一个属性设置了enumerable:false,那么:
- 在for-in循环和Object.keys()中不会出现
- 使用
obj.propertyIsEnumerable(prop)
可以检查对象上的属性是否可枚举。(不检查原型链)
遍历
使用for-in循环是无法直接获取属性值的,因为它遍历的是对象中所有可枚举属性,需要手动获取属性值。
第五章 原型
属性设置和屏蔽:
当给一个对象设置属性时,obj.a = 2
,过程比较复杂,:
- 如果obj对象中包含a属性,那么就直接修改已有的属性值
- obj对象中不包含a属性
- 原型链中也找不到a属性,a属性被添加到obj上。
- 原型链中有a属性
- 原型链中的a属性是普通数据属性,直接在obj上添加一个a属性,发生了屏蔽
- 原型链中的a属性被标记为只读,那么无法修改已有属性或者在obj上创建屏蔽属性。在严格模式下还会报错。总之这条语句会被忽略,不会发生屏蔽。
- 原型链中的a是一个setter,那么就会调用这个setter,但是a不会被添加到obj上。
原型继承:
Bar.prototype = Object.create(Foo.prototype)
Object.create(obj)
的作用是创建一个新对象,并指定这个新对象的原型对象为obj。
以下两种是常见的错误做法:
Bar.prototype = Foo.prototype
这只会让Bar.prototype直接引用Foo.prototype,当你为Bar.prototype添加属性时,实际上直接修改了Foo.prototype对象本身。如果你根本不需要Bar对象,那么直接使用Foo就好了。Bar.prototype = new Foo()
这是经典继承的做法,可以达到继承的目的,但是由于直接调用Foo函数,会有一些副作用,如为this(这里是Bar.prototype)添加实例属性,修改状态、注册到其他对象等,那么就会影响Bar的后代。
因此要创建一个合适的关联对象(继承),最好使用Object.create方法。
ES6提供了Object.setPrototypeOf()
方法来修改对象的原型,也可以直接使用这种方法来实现继承:
Object.setPrototypeOf(Bar.prototype,Foo.prototype);
与Object.create方法实现继承相比,这种方式不会重写Bar.prototype对象,而是直接设定其原型,但是Object.create则会创建一个新的对象赋值给Bar.prototype,不过这种缺点比较无关紧要。
类关系
function Foo(){……}
Foo.prototype.baz = ……
var a = new Foo();
首先可以使用instanceof来判断:
a instanceof Foo; //true
instanceof操作符左边是一个对象,右边是一个函数,主要判断a的原型链中是否有指向Foo.prototype的对象。
但是这种方法只能处理对象和函数之间的关系,不能判断两个对象之间有没有关联。
可以使用isPrototypeOf来判断。此例中是判断Foo.prototype与a的关系,因此可以这样使用:
Foo.prototype.isPrototypeOf(a); //true
isPrototypeOf关心的就是在a的原型链上是否出现过Foo.prototype.
可以在任意两个对象之间使用isPrototypeOf():
var b = {};
var c = Object.create(b);
b.isPrototypeOf(c); //true
我们可以直接获取一个对象的[[Prototype]]链:Object.getPrototypeOf(a) == Foo.prototype //true
.
很多浏览器也支持一种非标准的方法来访问内部的[[Prototype]]属性:a.__proto__ === Foo.prototype; //true
.__proto__
的实现大致上是这样的:
Object.defineProperty(Object.prototype,"__proto__",{
get:function(){
return Object.getPrototypeOf(this);
},
set:function(o){
Object.setPrototypeOf(this,o);
return o;
}
})
Object.create的polyfill:
Object.create = function(obj){
function F(){};
F.prototype = obj;
return new F();
}
Object.create还可以接收第二个参数,指定了需要添加到新对象中的属性名以及这些属性的描述符。
var obj = {a:2};
var anotherObj = Object.create(obj,{
b:{
enumerable:false;
writable:true,
configurable:false;
value:3
},
c:{
enumerable:true;
writable:false,
configurable:false;
value:4
},
});
anotherObj.hasOwnProperty(a); //false;
anotherObj.hasOwnProperty(b); //true;
anotherObj.hasOwnProperty(c); //true;
anotherObj.a; //2
anotherObj.b; //3
anotherObj.c; //4