作用域是什么
基础的编译原理,理解LHS(找到变量容器以便赋值),RHS(查找某个变量的值),了解作用域链。
了解在为什么要区分LHS和RHS非常重要,比如下面的代码
function foo(a){
console.log(a+b);
b=a;
}
foo(2);
第一次对b进行RHS会报错,undefined,因为这是一个未声明的变量,但下面的代码:
function foo(a){
b = a;
}
foo(2);
不会报错,因为在LHS时如果所有嵌套的作用域中都无法找到它,那么会在全局作用域中创建改变量。不过如果在严格模式下仍会报错undefined
。
词法作用域。
- 不要用eval和with,会导致作用域副作用,以及无法优化
函数作用域和块作用域
遵循最小暴露原则,规避冲突,可以用遮蔽变量(var i)。
为了避免全局变量污染,第三方库一般会在全局作用域中声明一个名字足够独特的变量,所有需要暴露给外界的功能都会成为这个对象的属性,另外可以用模块管理器来避免全局命名。
(function foo(){..})
作为函数表达式foo只能在..中被访问,外部作用域不行,foo也不会污染外部作用域函数表达式可以是匿名的,叫做匿名函数表达式,不过始终给函数表达式命名是个最佳实践
IIFE: 立即执行函数表达式 ->
(function foo(){..})();
还可以写成 (function foo(){..}()),功能是完全一致的let 可以让变量具有块级的作用域,而且变量不会被提升
提升
提升是个很坑的东西,注意以下几点:
变量的声明会被提升,但是变量的赋值不会,比如var a = 1; var a 会被提升到变量最上方,但赋值操作不会。声明在编译阶段,赋值在执行阶段
函数声明会被提升,但函数表达式不会提升。
具名的函数表达式,名称标识符在赋值之前无法在作用域中使用。即var foo = function bar(){..} 不能使用bar()进行调用,但在bar函数中可以调用。
函数声明和变量声明都会被提升,但函数会首先被提升,然后才是变量。所以变量和函数重复声明,变量的声明会被忽略掉
闭包
一个典型的闭包:
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2
// 另一个
function wait(message){
setTimeout(function time(){
console.log(message);
}, 1000);
}
wait("hello clousre");
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
闭包的神奇之处在于可以阻止foo的内部作用域被销毁,没有被回收,bar依然持有对该作用域的引用,而这个引用就叫做闭包
闭包使得函数可以继续访问定义时的词法作用域
本质上无论何时何地,如果将(访问它们各自词法作用域的)函数当做第一级的值并导出传递,就会看到闭包在这些函数中的应用
那么闭包有哪些应用呢?其实包括定时器,事件监听器,Ajax请求,跨窗口通信,Web Workers或者任何其他的异步(或者同步)任务中,只要使用回调函数,实际上就是在使用闭包!
从技术上来讲,闭包是发生在定义时的,IIFE是最常用来创建可以被封闭起来的闭包的工具
一个典型的易错场景循环闭包,我们需要用IIFE为每一个迭代生成一个新的作用域,并在作用域中保存正确的值,如下:
for( var i = 1; i <=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log(j);
}, 1000 * j);
}
)(i);
}
- let可以劫持块作用域,并且在这个块作用域中声明一个变量,本质上是将一个块转换成了一个可以被关闭的作用域,for循环头部的let变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量,所以上面的代码可以用下面的代码代替:
for(let i=1; i<6; i++){
setTImeout(function time(){
console.log(i)
}, i*1000)
}
- 模块机制利用了闭包的强大威力。模块的两个特征:
为内部作用域调用一个包装函数;
包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包.
this
this既不指向函数自身,也不指向函数的词法作用域
this实际上是在函数被调用时发生的绑定,它指向什么全完取决于函数被调用的位置。
this有4条绑定规则
默认绑定,this指向全局对象
隐式绑定,当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。不过隐式绑定经常丢失this绑定
显式绑定,call和apply方法,第一个参数是对象,是给this准备的,如果是原始类型会被装箱。硬绑定即bind方法,创建一个包裹函数,返回一个显式指定上下文的函数。相应的还有API调用如forEach(foo,obj)//把this帮到obj
new绑定,使用new时对函数的构造调用会创建一个全新的对象,这个新对象会绑定到函数调用的this,如果函数没有返回其他对象那么自动返回这个新对象。
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()
绑定例外
bind可以对参数柯里化(预先设置一些参数)
var bar = foo.bind(null,2);
apply会把数组展开成参数
foo.apply(null, [2,3]);
ES6中可以用
...
操作符来代替apply(..)
,可以避免不必要的this绑定如果函数中确实使用了this,而传入了null,那么this将绑定到全局对象,可能造成不可预计的后果。更安全的this使用
Object.create(null)
创建一个新的对象,它和{}很像,不过不会创建一个Object.prototype
这个委托,可以用ø(option+o)
来表示.如果创建了函数的间接引用,调用这个函数会应用默认绑定规则
在箭头函数中的this根据外层作用域来决定this,箭头函数的绑定无法被修改,它使用了更常见的词法作用域来取代了传统的this机制。我们最好选择一种风格来编写代码,避免混用,推荐使用ES6的词法作用域
对象
JavaScript的6种主要类型: string, number, boolean, null, undefined, object。
简单基本类型string, number, boolean, null, undefined本身不是对象,null有时会被当做一种对象类型,但确实语言的一个bug, typeof null会返回”object”。
JavaScript中并不是万物都是对象的,不过有很多特殊的对象子类型,比如函数和数组。
常见内置对象String,Number,Boolean,Object,Function,Array,Date,RegExp,Error,他们实际上只是一些内置函数,被当做构造函数来使用.
语言在必要时会自动把字符串字面量转换成一个String对象,于是我们能够调用一些String的方法,比如length,charAt
在对象中,属性名永远都是字符串,其他值会被首先转换为字符串,比如true => “true”, 3 => “3”, myObject = {} ,myObject => “[object Object]”
ES6中增加了 “可计算属性名”,可以在文字形式中使用[]包裹一个表达式来当做属性名
函数式不”属于”一个对象的,只是对于相同函数对象的多个引用
深拷贝不易实现,对于JSON安全的对象来说可以用
var newObj = JSON.parse(JSON.stringify(someObj))
来实现ES6提供了浅复制Object.assign(..),比如var newObj = Object.assign({}, myObject);
从ES5开始所有属性有了属性描述符,比如writable,configurable,enumerable
writable为false的话对属性赋值会静默失败,在严格模式下会出错
configurable为false的话不可撤销,重新配置会出错,而且会禁止删除这个属性,delete会静默失败
enumerable控制属性是否会出现在对象的属性枚举中,比如for…in
不可变,可以设置writable和configurable创建对象常量,用Object.preventExtensions禁止扩展,用Object.seal创建密封对象,用Object.freeze冻结一个对象
在对象中没有找到名称相同的属性,[[Get]]操作会返回undefined,如果引用了一个当前词法作用域中不存在的变量,会抛出一个ReferenceError异常
getter和setter分别实现[[Get]]和[[Put]]
如何判断一个属性是否在一个对象中存在。可以用in操作符,(“a” in myObject)和myObject.hasOwnProperty(“a”),区别是in会查看原型链,如果对象没有Object.prototype的委托(比如Object.create(null))可以用Object.prototype.hasOwnProperty.call(myObject,”a”)来显式绑定
propertyIsEnumable检查给定对象是否直接存在于对象中而且enumerable: true
Object.keys(..)返回一个数组,包含所有可枚举属性
getOwnPropertyNames(..)返回一个数组,包含所有属性,无论是否可枚举
forEach遍历数组的所有值并忽略回调函数的返回值
every(..)一直运行直到回调函数返回false
some(..)会一直运行直到回调函数返回true
for…of用于直接遍历数组的值,由Array的迭代器对象实现的next方法决定何时结束
类
类: 继承,封装,多态。多态: 父类的通用行为可以被子类用更特殊的行为重写
其他语言中的类和JavaScript中的“类”并不一样
类是一张蓝图,按类来实例化一个可交互的对象
mixin(混入)用来模拟类的复制行为
显式混入,尽量避免使用显示伪多态以及混合复制,可以用用寄生继承[(复制一份父类对象的定义混入到子类的定义中,然后用复合对象构建实例)]
隐式混入,绑定子类的对象的this到父类中,然后调用父类的方法将操作应用到子类中,也应该尽量避免
原型
所有普通的[[Prototype]]链最终都会指向内置的Object.prototype。
myObject.foo = “bar”,属性设置时,如果myObject包含foo属性,那么只会修改该值,如果不直接在myObject中则会遍历[[Prototype]]链,如果没有找到,那么属性会直接添加到myObject上,如果foo存在于上层原型链中,那么如果myObject也包含foo属性,那么会发生屏蔽,myObject中的foo屏蔽所有上层foo属性,如果foo不直接存在于myObject中那么会出现三种情况:
原型链上层存在foo普通数据访问属性,没有标记为只读,那么在myObject中添加一个名为foo的新属性,屏蔽
如果原型链上层的foo是只读的,那么不发生屏蔽,赋值被忽略,如果在严格模式下会出错
如果原型上层存在foo且为setter,那么一定会调用setter.
如果希望在2,3情况下也屏蔽foo,那就不能使用=操作符,而使用Object.defineProperty(..)来向myObject添加foo
myObject.a会造成隐式屏蔽,修改委托属性一定要小心,如果要让委托属性的a值增加,唯一的方法是anotherObject.a
JavaScript中只有对象,他可以不通过类直接创建对象,对象直接定义自己的行为.
所有的函数默认都会拥有一个名为prototype的公有并且不可枚举的属性,它指向另一个对象,这个对象就称为函数的原型
这个对象是在调用new Foo()时创建的,最后会被关联到Foo.prototype对象上.在调用 a = new Foo()的时候其中一步就是将a内部的[[Prototype]]链接到Foo.prototype所指向的对象
原型继承和面向对象中的继承完全不是一个东西,继承意味着复制操作,JavaScript默认并不会复制对象属性,相反JS会在两个对象之间创建一个关联,用委托解释更合适
把委托行为归结到对象本身并且把对象看做是实物,就差不多可以理解差异继承了
Foo.prototype.constructor === Foo // true, 调用new Foo()创建的对象也有一个.constructor属性,指向创建这个对象的函数
对JS中”构造函数”最准确的解释是所有带new的函数调用,函数不是构造函数,但是当且仅当使用new时,函数调用会变成“构造函数调用”
Foo.prototype.myName = …会给Foo.prototype对象添加一个函数属性,在创建a,b的过程中,a和b的内部[[Prototype]]都会关联到Foo.prototype上a,当a和b中无法找到myName时,它会通过委托在Foo.prototype上找到。
a.constructor引用只是被委托给了Foo.prototype,并不是a本身具有constructor引用,如果把Foo.prototype换成其他对象,那么a的constructor会指向Object,因为委托给了原型链的顶端
a1.constructor是一个非常不可靠并且不安全的引用,通常要尽量避免
两种把Bar.prototype关联到Foo.prototype的方法:
Bar.prototype = Object.create(Foo.prototype) // ES6之前会抛弃默认的Bar.prototype
Object.setPrototypeOf(Bar.prototype, Foo.prototype) //ES6开始直接修改现有Bar.prototype如何找出a的祖先?
a instanceof Foo
,判断在a的整条[[Prototype]]链中是否有指向Foo.prototype的对象Foo.prototype.isPrototypeOf(a)
,判断在a的整条[[Prototype]]链中是否出现过Foo.prototype
Object.getPrototypeOf(a) 用于直接获取一个对象的[[Prototype]]链
绝大多数浏览器(非所有)支持一种非标准的方式a.proto来访问对象的[[Prototype]]对象
var bar = Object.create(foo)会创建一个新对象并关联到foo上,这样可以充分发挥[[Prototype]]机制的为例避免不必要的麻烦(比如new的.prototype和.constructor引用)
内部委托(在对象内部新增api引用委托对象的api)比直接委托让API接口更加清晰
行为委托
JavaScript原型链的本质是对象之间的关联关系
委托行为意味着某些对象在找不到属性或者方法引用时会把这个请求委托给另一个对象
禁止互相委托
委托模式更适合JavaScript,避免丑陋的显式伪多态调用(Widget.call,Widget.prototype.render.call)
类构造函数需要在同一个步骤中实现构造和初始化,很多时候把两步分开更灵活,委托模式可能使用
var btn1 = Object.create(Button); btn1.setup(125,30,'Hello');
来调用,虽然多了一些代码,不过我们可以根据需要让他们出现在不同的位置很多情况下委托只需要两个实体(兄弟关系,互相委托),而类设计需要三个(一个基类,两个子类)
使用委托,我们不需要实例化类,因为根本不是类,只是对象,也不需要合成,因为两个对象之间可以通过委托相互合作。避免使用了类设计模式中的多态,子类不需要复写父类的api,可以用自己的api更贴切的描述他们的行为。
作者不推荐使用ES6的class特性。ES6中可以在任意对象的字面形式中使用简洁方法声明
var loginController = {
getUser() { // 不用写function
...
}
}
此外ES6中可以用Object.setPrototypeOf(..,..)来进行对象关联
简洁方法在需要自我引用的时候最好使用传统的具名函数表达式来定义
内省,检查实例的类型
“鸭子类型”: 如果看起来像鸭子,叫起来像柚子,那就一定是鸭子,比如:
if(a1.something){
a1.something();
}
鸭子类型风险挺大,比如判断Promise的时候通过检查对象是否有then方法,是很有风险的
通过这本书,作者想跟我们说的是想在js中使用面向对象是很操蛋的,还是使用对象关联好,行为委托比类模式更适合JS
ES6的class原来也有很多问题…,比如你用C.prototype.rand()可以改变类C的rand方法,那么所有子类和实例都会受到影响,因为class只是语法糖,内部实现仍然是[[Prototype]].另外可能出现属性屏蔽,比如constructor(id)和id方法,那么属性会屏蔽id方法,super方法在声明时静态绑定可能引起问题.
JavaScript最强大的特性之一就是它的动态性,不过class似乎在说: 动态太难实现了,我们假装成静态吧(但实际上并不是!)
诶!