js基础知识深入:作用域,闭包,this。对象,类,原型,对象关联,委托

作用域


例: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
  • 1处包含着全局作用域,有foo
  • 2处包含foo创建的作用域,有a, b,bar
  • 3处包含bar所创建作用域,有c

作用域查找会在找到第一个匹配的标识符停止,无论函数在哪里被调用,被如何调用,它的词法作用域只由函数被声明时所处位置决定

函数作用域

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)
  • 立即执行函数(IIFE)
(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


this

es6添加了一个特殊语法用于函数声明,叫做箭头函数,箭头函数在涉及this绑定时,用当前的词法作用域覆盖this本来的值。 或者用var self = this 或者用.bind(this)

判断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  可配置
  }
)

配置属性。

  • writable:false 不可写,即定义好value后,值不可修改,严格模式下修改会报错
  • configurable:false 不可配置,即不可再使用Object.defineProperty()配置属性,具体是配置enumerable和configurable会报错,修改value还是可以的,是单向操作,无法撤销!!!,仍可以把writable由true改为false,但不可false改true,且不可delete属性
  • enumerable,就是对象的for...in枚举,如果不想某个属性被枚举出,可设置false。

对象的不变性

  1. 对象常量:结合writable:false和configurable:false,可以创建一个真正的常量属性,不可修改,重定义,删除
  2. 禁止扩展:Object.preventExtensions(obj),严格模式下添加新属性会报错,
  3. 密封:Object.seal(obj),不能添加,配置和删除属性,但是可以修改,该方法实际上会调用Object.preventExtensions(obj),并把所有属性标记为configurable:false
  4. 冻结:Obejct.freeze(obj),实际会调用Object.seal(obj)并把所有属性的writable标记为false,禁止对任意直接属性的修改

以上四种方法只是冻结直接属性,如果对象引用了其他对象如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()

如何让查找到a的“祖先?(委托关联)呢?

1.a instanceof Foo // true,instanceof回答的问题是:在a的整条原型链中是否有Foo.prototype指向的对象。这个方法只能处理对象a和函数(带.prototype引用的Foo)的关系,不能处理对象和对象

2.Foo.prototype.isPrototypeOf(a) // true,回答的问题是:在a的整条原型链是否出现Foo.prototype,这个方法可以判断对象和对象指向的关系

获取对象原型链

  1.  
Object.getPrototypeOf(a)
Object.getPrototypeOf(a) === Foo.prototype // true
  1.  
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的原型链本质就是行为委托机制。我们可以努力实现类的机制(构造函数)也可以拥抱更自然的原型链委托机制。

你可能感兴趣的:(js,javascript,设计模式)