例:var a = 2
1,编译器先询问作用域是否有变量a,有则忽略,继续编译,无则要求作用域在当前作用域声明一个变量a
2,编译器为引擎生成运行所需代码,处理a=2这个赋值操作,引擎先询问作用域是否有a,有则使用,无则继续找。如果最终找到a,把2赋值给a,否者抛出异常。
当变量出现在赋值操作左侧为LHS查询,右侧为RHS查询,LHS表示赋值操作的对象目标是谁,RHS是查询。
所以说,所谓作用域是根据名称查找变量的一套规则,在何处以及如何查找变量的规则。当一个块函数嵌套另一个,发生作用域嵌套,如果当前作用域无法找到某个变量,会向上一级查找,指导找到,或抵达全局作用域。
作用域分词法作用域和动态作用域。常用词法
就是定义在词法阶段的作用域,由你在写代码时将变量和块作用域写在哪里决定的。
例:
function foo(a) { // ==1==
var b = a * 2; // ==2==
function bar(c) { // ==3==
console.log(a, b, c);
}
bar(b * 3)
}
foo(2) // 2, 4, 12
作用域查找会在找到第一个匹配的标识符停止,无论函数在哪里被调用,被如何调用,它的词法作用域只由函数被声明时所处位置决定
函数作用域
function foo(a) {
var b = 2;
function bar() {
console.log(b);
}
var c = 3;
}
全局作用域有自己的作用域气泡包裹foo,foo包裹a,b,c,bar。foo内可访问它们,bar内也能访问,但是在foo外不能访问。子可以向上访问父,父不能访问子。
函数作用域的含义是:属于这个函数的全部变量都可以在整个函数范围内使用及复用。
我们可以将任意代码片段用函数包裹,在外部作用域无法访问。但是这就要声明一个有名函数,这会对其所在作用域‘污染’,无名则更理想。
有2个方案:
setTimeout(function() {
console.log(99)
}, 1000)
(function() {
var a = 3;
console.log(a)
})()
或者
(function() {
var a = 3;
console.log(a)
}())
两种一样
块作用域,变量和函数属于某个代码块,通常指{...}
ES6引入了块级作用域,let 和 const,声明变量不会再提升,只在当前块起作用。let可以修改值,const不可以修改及重新赋值
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var q = foo()
q() // 2 ==这就是闭包==
函数bar可以访问foo内部作用域,然后我们将bar当作返回值return,foo执行并赋值给q,就是返回值bar赋值给q,再执行q。此时我们在词法作用域以外的地方访问到了foo内部的a。通常foo执行后会被垃圾回收机制回收,因为看上去不会再被使用,但是闭包阻止这件事发生,foo内部作用域依然存在,bar在使用,bar依然持有对该作用域的引用,这个引用叫闭包,因此之后q被调用,可以访问a
es6添加了一个特殊语法用于函数声明,叫做箭头函数,箭头函数在涉及this绑定时,用当前的词法作用域覆盖this本来的值。 或者用var self = this 或者用.bind(this)
1.函数是否在new中调用,(new绑定)?如果是,this绑定的是新创建的对象。var bar = new foo()
2.函数是否通过call。apply(显示绑定)或者硬绑定(bind)?若是,this绑定的是指定的对象。var bar = foo.call(obj)
3.函数是否在某个上下文对象中调用(隐式绑定),若是,this绑定的是那个上下文对象 var bar = obj.foo()
4.如果都不是,使用默认绑定,严格模式下绑定到undefined,否则绑定到全局 var bar = foo()
注意,如果把null或undefined作为this的绑定对象传入call,apply,bind,这些值在调用时会被忽略,实际应用的是默认规则。如:
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
但这会导致一些难以分析和追踪的bug
更安全的this
传入一个特殊对象,如一个空对象Object.create(null),比{}更空,因为不会创建Object.prototype。可以赋予一个特殊变量,如ø。
function foo(a,b) {
console.log(`${a}: a, ${b}: b`)
}
var ø = Object.create(null)
//把数组展开成参数
foo.apply(ø, [2,3]) // a: 2, b: 3
使用bind(...)进行柯里化
var bar = foo.bind(ø, 2)
bar(3) // a: 2, b: 3
ES6的箭头函数并不会使用上面的四个规则,而是根据当前词法作用域决定this,会继承外部函数调用时的this绑定,无论绑定到什么,无法修改
创建对象方法很多,如字面量,工厂模式,构造函数,new Object(),Object.create() var obj = {a:2} obj.hasOwnPropertu('a') // true (只在当前对象的属性中查找,但是如果是var obj = Object.create(null),这种情况,会失败。所以更强硬的方法是用Object.prototype.hasOwnProperty.call(obj, 'a')
('a' in obj) // true(会检查是否在当前对象以及它的原型链上)
属性描述
ES5开始,所有属性具备描述符
一个对象的属性的默认配置如下:
var obj = { a: 2 }
Object.getOwnPropertyDescriptor(obj, 'a')
/*
* {
* value: 2, 值
* writable: true, 可写
* enumerable: true, 可枚举
* configurable: true 可配置
* }
*/
可以用
Object.defineProperty(obj, 'a',
{
value: 2, 值
writable: true, 可写
enumerable: true, 可枚举
configurable: true 可配置
}
)
配置属性。
对象的不变性
以上四种方法只是冻结直接属性,如果对象引用了其他对象如obj.arr = [1,2],仍可以对数组进行插入删除操作。可以遍历它引用的所有对象并在这些对象上调用Object.freeze(),但是要小心,可能会无意间冻结其他(共享)对象。
类是一种设计模式,类的概念来自房屋建造,建筑设计师会出一个建筑蓝图,规划建筑的特性,长宽高,几窗户,门,材料等等,不关心建筑被建在哪,也不关心造多少个。需要工人按照图纸施工,把蓝图上的建筑复制到现实,建筑就是蓝图的实例。一个类就是一张蓝图,一个建筑就是一个实例。我们可以在实例调用方法并访问其所有公有属性。
类实例由一个特殊类方法构造,方法名通常和类名相同,叫构造函数。通过new来创建一个实例。
在面向类的语言中,可以先定义一个类,然后定义一个继承前者的类,后者通常被称为子类,前者为父类,相对父类,子类是一个独立完全不同的类,它包含父类行为的原始副本,但是也可以重写所有继承的行为甚至重新定义行为,不会影响父类,互不影响,在子类中也可以相对引用它继承的父类,这种相对引用通常被称为super(超类),这种技术被称为多态或者虚拟多态。类的继承实质就是复制,子类得到的是父类的一份副本。
继承多个类
在js中继承或者实例时,js对象机制并不会自动执行复制行为,简单来说,js中只有对象,并不存在可以被实例化的‘类’,一个对象并不会被复制到其他对象,而是会被关联起来。由于在其他语言中表现的都是复制行为,因此js开发者也想出一个方法来模拟复制,混入(分为显式和隐式),但是通常会产生丑陋脆弱的语法,让代码难以维护,此外,无法完全模拟类的复制,因为对象(和函数,函数也是对象)只能复制引用,无法复制其本身。得不偿失,会有隐患。
ES6引入类Class,https://es6.ruanyifeng.com/#docs/class
js中的对象有个特殊的[[Prototype]]内置属性,其实就是对于其他对象的引用。当获取对象属性的值时(obj.a),会先检查对象本身有则使用,没有则查找原型链。 要注意的是,当使用for...in枚举属性时,也是使用这个流程,如果只想枚举对象本身属性,可以添加Object.hasOwnProperty(key)来判断。
obj.a = 2,如果obj中有a这个属性,则这个赋值语句会修改a,如果本身没有,原型链上也没有,则会把a添加到obj。如果本身有,原型链上也有,则obj本身的a属性会屏蔽原型链上所有a,因为obj.a总会选择原型链中最底层的a属性
在JS中,只有对象,对于实例化,并没有类似的复制机制,只能创建多个对象,新对象内部连接[[Prototype]]关联的上原对象的prototype
function Foo() {
// ...
}
var a = new Foo()
Foo.prototype.constructor === Foo // true
a.constructor === Foo // true
Foo.prototype有一个公有且不可枚举的属性,.constructor,引用的是对象关联的函数(本例为Foo)。 对于Foo,它只是一个普通的函数,函数不是构造函数,但是当且仅当使用new时,函数调用会变成“构造函数调用”
看这段代码:
function Foo() {}
Foo.prototype = {}
var a1 = new Foo()
a1.constructor === Foo // false
a1.constructor === Object // true
如果你以为Foo构造了a,所以a.constructor === Foo // true,但是实际上,a并没有constructor这个属性,它会委托原型链上的饿Foo.prototype,所以全等,但是在a1中,Foo.prototype={},是个空对象,找不到constructor这个属性,所以为false,在原型链上的Object上找到了constructor,所以a1.constructor === Object // true
可以目标constructor不可枚举,但是可以修改,可以原型链上任意对象添加constructor属性赋值,是个不可靠不安全的属性,尽量避免使用这些引用。
原型继承最好用Object.create(...),如Bar.prototype = Object.create(Foo.prototype),这是创建了一个新对象并把旧对象抛弃,不能直接修改,然后关联到Foo.prototype。在ES6中添加了Object.setPrototypeOf(...)函数,可以用可靠的方法来修改关联。如:
let breakfast = {
getDrink(){
return 'tea'
}
}
let dinner ={
getDrink(){
return 'bear'
}
}
//下面使用Object.create()方法创建一个关联breakfast的对象sunday,可以看到sunday的getDrink()方法返回的就是breakfast对象里的getDrink()方法返回的字符串
let sunday = Object.create(breakfast)
console.log(sunday.getDrink()); //tea
//判断sunday这个对象的prototype是否等于breakfast
console.log(Object.getPrototypeOf(sunday)===breakfast) //true
//重新设置一下sunday的prototype,用的是Object.setPrototypeOf(),第一个参数是要设置的对象sunday,第二个参数是要设置成的那个prototype对象dinner,返回sunday.getDrink()
Object.setPrototypeOf(sunday,dinner);
console.log(sunday.getDrink()); //bear
console.log(Object.getPrototypeOf(sunday)=== dinner); //true
function Foo() {
...
}
var a = new Foo()
1.a instanceof Foo // true,instanceof回答的问题是:在a的整条原型链中是否有Foo.prototype指向的对象。这个方法只能处理对象a和函数(带.prototype引用的Foo)的关系,不能处理对象和对象
2.Foo.prototype.isPrototypeOf(a) // true,回答的问题是:在a的整条原型链是否出现Foo.prototype,这个方法可以判断对象和对象指向的关系
Object.getPrototypeOf(a)
Object.getPrototypeOf(a) === Foo.prototype // true
a.__prototype__
a.__prototype__ === Foo.prototype // true
现在知道,[[Prototype]]机制就是存在于对象中的一个内部链接,它会引用其他对象,通常来说,这个对象作用是:如果在对象上没有找到需要的属性或者方法引用,引擎就会继续在[[Prototype]]关联的对象上进行查找,同理,如果后者中也没找到需要的引用,就会继续查找它的[[Prototype]],以此类推,这一些列对象的链接被称为“原型链”
var foo = {
something: function() {
console.log('hello')
}
}
var bar = Object.create(foo)
bar.something() // hello
Object.create(foo)会创建一个新对象bar关联到foo,这样充分发挥了[[Prototype]]机制,且避免不必要的麻烦(比如使用new的构造函数调用会生成.prototype,.constructor的引用)
关联关系是备用
看起来对象直接的关联是处理“缺失”属性或方法的一种备用选项,有点道理,但不是[[Prototype]]的本质。 思考下面代码
var anotherObj = {
cool: function() {
console.log('cool')
}
}
var myObj = Object.create(anotherObj)
myObj.cool() // cool
由于原型链,myObj.cool() 可以调用成功,但是你如果这样写只是为了让myObj在无法处理属性或方法时可以使用备用的anotherObj,那么程序就会有点“神奇”,而且很难维护,在ES6有个“代理”,Proxy的高端功能就是实现“方法无法找到的行为”。
可以这么设计:
var anotherObj = {
cool: function() {
console.log('cool')
}
}
var myObj = Object.create(anotherObj)
myObj.doCool = function() {
this.cool() // 内部委托
}
myObj.doCool() // cool
此处调用的doCool是存在myObj的,这样设计更清晰,遵循委托设计模式。内部委托比直接委托更清晰。
之前的类已经说过, 她是类和继承的设计模式,定义一个通用父类,定义子类继承父类,重写或者添加。
现在来看看委托,定义一个名为Task对象,包含所有任务都可以使用(写作使用,读做委托)的具体行为,然后对于每个任务定义一个对象存储数据和行为,把特定的任务对象都关联到Task对象上,让任务对象在需要时可以进行委托。
案例:
Task = {
setID: function(ID) { this.id = ID },
outputID: function() { console.log(this.id) }
}
// 让XYZ委托Task
XYZ = Object.create(Task)
XYZ.prepareTask = function(ID, Label) {
this.setID(ID)
this.label = Label
}
XYZ.outputTaskDetails = function() {
this.outputID()
console.log(this.label)
}
Task和XYZ并不是类(或者函数),它们是对象,XYZ通过Object.create()创建,它的[[Prototype]]委托了Task对象。这种发方式也可以叫对象关联。
禁止相互委托,B关联到A,然后A关联到B会出错,如果你引用类一个两边都不存在的属性或方法,那么就会在原型链上产生一个无限递归的循环。
用对象来设计代码,会更简洁更清晰,行为委托是对象之间的关联,是兄弟关系,而类是父类和子类的关系。js的原型链本质就是行为委托机制。我们可以努力实现类的机制(构造函数)也可以拥抱更自然的原型链委托机制。