JS U DONT KNOW 简单笔记(1)this & prototype

162 阅读10分钟

第一册

第一章 作用域

1.2 理解作用域

引擎,编译器,作用域 。是JS代码运行中的三剑客。 var a = 2;在运行时编译器先查询作用域 a是否已被声明,然后引擎再去作用域查 a 最后赋值。三步骤。


LHS和RHS。“赋值操作的目标是谁(LHS)” 以及“谁是赋值操作的源头(RHS)”

1.3 作用域嵌套

1.4 异常

ReferenceError 和 TypeError的区别

第二章 词法作用域

2.2 欺骗词法--eval(),副作用无法优化

第三章 函数作用域和块作用域

作用域只能向上查找,不能向下。函数作用域的上级是看它定义的时候的位置。

函数申明和函数表达式

匿名函数表达式

  • 书写方便
  • 调试困难,栈追踪不会显示有意义的函数名
  • 可读性

具名函数表达式是最佳解决方案

立即执行函数

  • Immediately Invoked Function Expression
  • 有两种格式,双括号或者function(){}
  • 主要用法是直接当作参数传递

let和块作用域

在ES6之前解决块作用域只能只用函数包裹,with,try/catch这几种办法,很不方便。在ES6引入了let。let可以把变量绑定到所在的任意作用域中,

  • 垃圾收集: 和闭包有关,将占据大量内存的数据或者其他功能放在一个显示块中使用let执行,这样可以及时回收
  • let可以将for表达式中的计数器绑定到循环的每一个迭代中,也和闭包有关
  • 更健壮的块作用域使用方式,附录B

第四章 提升

参考这一段代码,提升的只有变量申明,也就是var a;这一句而已。不太准确。

参考第一章的关于编译器工作流程, 引擎会在解释 JavaScript 代码之前首先对其进行编译。 编译阶段中的一部分工作就是找到所有的声明, 并用合适的作用域将它们关联起来 。提升的正确描述是包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。var a = 2;会被分解成申明和赋值两端,申明提前,赋值留在原地执行。

函数提升优先级比变量高。但是函数表达式不会提升。

给一个变量赋值一个空函数,本质上还是undefined,因此对undefined进行函数调用,会导致非法函数。即使是具名的函数表达式,名词标识符在赋值之前也不能在作用域中使用。

练习 :

0c1fb9552f0886dd2b61e6e8f91e1075.png

4.3 函数优先

之前已经提过函数优先提升,下面结合例子:

foo(); //1
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仅此而已,等到赋值后,才能正常使用。

要特别注意避免重复申明,特别是当普通的var声明和函数声明混合在一起。

第二册

第一章

  • 在具名函数内部,可以使用函数同名的变量来指代自身(this),从而避免this的使用,但是这不解决核心问题。
  • 任何时候,this都不指向函数的词法作用域

参考以下代码:

  • 调用bar()最简单的方法是直接省略之前的this。this这里在直接调用的时候指向winodw,使用new创建obj的时候指向函数自己
  • 其次,this不能用来引用一个词法作用域内部的东西。

第二章

分析this首先要分析调用栈和调用位置, this运行时绑定

2.1 默认绑定---window

严格模式下可以使默认绑定失效,但是必须是全局严格或者函数自身严格,函数自身非严格但是在严格模式下被调用时,默认绑定还是生效的。 严格非严格混用是非常不好的开发习惯。

2.2 隐式绑定

若函数是某个对象的属性,this指向这个obj。在链式引用中,只和最后一个有关。

隐式丢失

虽然bar是obj.f oo的一个引用,但是实际上它引用的是foo函数本身,因此应用了默认绑定。参数传递是另一种隐式引用,也会隐式丢失。

2.3 显式绑定

  • bind
  • apply
  • call 这三种属于显式绑定,传入的第一个参数会被当做this,若是原始类型,则会被打包成对象形式,这就是装箱。 但是显式绑定无法解决绑定丢失的问题。我们需要硬绑定,在一个函数内部完成绑定过程,然后使用封装的函数,这样就解决了绑定丢失的问题。
    或者使用绑定辅助函数

2.4 new 绑定

JS中没有真正的构造函数,只有函数构造。

  1. 创建一个新的对象
  2. 这个新对象会被执行原型连接
  3. 将这个新对象绑定到函数调用的this
  4. 如果这个函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象

2.5 优先级

在上面介绍的四个规则:

  • 默认绑定
  • 隐式绑定
  • 显式绑定
  • new绑定

这四个情况中的数个可能一起发生,那么怎么判断优先级呢? 显式绑定 > 隐式绑定 | new绑定 > 隐式绑定 new绑定会创建一个新对象,因此和显式绑定bind混用的时候往往出乎意料,其并不修改而是创建。

bind函数的另一个功能是可以创建偏函数:

function foo(p1,p2){
    this.val = p1 + p2;
}
var bar = foo.bind(null, "p1");//this here is not important and it will re-bind when use new
var baz = new bar("p2")
baz.val;//p1p2

2.6 绑定意外

  • 当call,apply,bind的this位是 null或者undefined时,应用的是默认绑定。当使用apply传入数组展开做参数的时候,往往使用null占位。 但是在ES6中提供了 展开扩展符,这种方法被淘汰力
  • 我们还可以使用空对象Object.create(null)来让代码更加安全

2.7 箭头函数

另一种规则,更具定义的作用域决定this

第三章 Class Object

deepcopy && shallowcopy

首先要注意,拷贝指的是对象,基本类型不存在拷贝。对象在栈,基本类型在堆。 浅拷贝只复制指向内存的指针,变量共享内存,一个更改了值,另一个也会更改 深拷贝复制并创造一个新变量,开辟一块新的内存空间,修改相互不影响,

Object.assign()

可以实现浅拷贝的功能

slice和concat

arr.slice() arr.concat()可以实现数组的一维深拷贝,意思是数组中不包含任何对象元素的情况下的深拷贝

ES扩展运算符

和slice concate一样,可以实现一维的deepcopy

deepcopy

定义一个新的对象,遍历源对象的属性并且赋值给新对象的属性

  • 利用递归实现每一层都重新创建对象并赋值
  • 利用JSON对象中的parse和stringify

递归

function deepcopy(obj){
    const targetObj = obj.constructor === Array? [] ; {};
    for(let keys in obj){
        if(obj.hasOwnProperty(keys)){ //不需要原型链上的属性和方法
            if(obj.keys && typeof obj.keys === "object"){
                targetObj[keys] = deepcopy(obj[keys]);
            }else{
                targetObj[keys] = obj[keys]
            }            
        }
    }
    return targetObj;
}

JSON.parse(JSON.stringify(obj))

先将obj转为JSON字符串,然后再解析该字符串转化成OBJ。但是有一定的局限性,undefined,symbol,function,regrex不能被正确地识别,会被忽略。

function deepClone(ojb){
    let targetObj = {};
    try{
        targetObj = JSON.parse(JSON.stringify(obj));
    }catch(e){
        console.log(e)
    }
    return targetObj;
}

原型

对象属性赋值过程

myObj.foo = "bar" 这么一条语句的实现过程是什么呢?

  • 若myObj有一个同名属性且未被设置为只读,会更改值。 若是只读属性,忽略该语句
  • myObj本身且原型链上无同名属性,则在myObj上添加
  • 若myObj有,原型链上也有,原型链上的会被屏蔽
  • 若原型链上存在foo且它是一个setter,那么该setter会被调用,foo不会被添加到myObj也不会更改setter的定义

若希望安全地实现对myObj添加属性,要使用Object.defineProperty。 只读属性会屏蔽下层对象创建同名对象,一定要注意,而且这种限制只存在 =赋值 中。

var anotherObject = {a:2}
var myObject = Object.create(anotherObject);
anotherObject.a //2
myObject.a//2
myObject.hasOwnProperty('a');//false
myObject.a++;//隐式屏蔽

anotherObject.a//2
myObject.a//3
myObject.hasOwnProperty("a")//true
思考myObject.a++中发生了什么?

JS没有类

JS中,类无法描述对象的行为,对象直接定义自己的行为。而“类似类”的行为利用了func有一个公共且不可枚举的prototype属性,这个属性又指向另一个对象。一般称其为函数的原型,但是“被贴上函数的原型标签的对象”可能更加准确。 其他语言中,实例化意味着把类的行为规范复制到一个物理对象或者新类上,但是JS中不能创建一个类的多个实例,只能创建多个对象,这些对象的原型是同一个。

new 的四个步骤

  • 创建一个新对象
  • 对这个新对象进行原型链接
  • 这个新对象会被绑定到函数调用的this
  • 若这个构造函数不返回其他对象,则返回这个新对象

更准确地描述new,其创建了一个关联到其他对象的新对象。并不复制,而只是关联。这一过程称为原型继承。

实际上,所有new 调用的函数都会被认为是构造函数,其会返回一个新对象,即使函数本身并无这种操作。

函数不是构造函数,当且仅当使用new,函数会变成构造函数

Foo.prototype.constructor === Foo

function Foo(){
    ...
}
Foo.prototype.constructor === Foo; //true
var a = new Foo();
a.constructor === Foo;//true;

虽然这个等式成立,但这并不表示a由Foo构造

如何给自定义Foo.prototype添加constructor

a.consrtuctor 并不安全可以被随意修改,尽可能避免使用

原型继承

ES6之前的风格

没有extends和super(), 而是直接使用call绑定Foo,然后将Foo的原型与Bar的原型连接,这样Bar就可以通过其原型访问Foo的原型中的属性和方法。 通过这种链接,Bar的显式原型的隐式原型等于Foo的显式原型

Bar.prototype.__proto__ == Foo.prototype //true

上面这种做法等价于下面这种:

错误做法

它们会引发什么后果? P155

反射

反射(reflect, introspection) 是检查一个类所继承的父类的操作。

  • a instanceof Foo 检查a的原型链中是否有Foo
  • 但是这只能判断对象和函数的关系,怎么判断2个对象之间呢?
  • Foo.prototype.isPrototypeOf(a)
  • 在a的整条原型链中,是否存在锅Foo.prototype

对象关联----对象之间的关系是委托不是复制

现在是时候弄明白Object.create()。其会创建一个新对象并把其关联到我们指定的对象foo,这样我们就可以充分发挥Prototype的威力,并且避免new的副作用( 使用new 会生成.prototype 和 .constructor)

和new不同的是, new后面应该是一个函数,而Object.create() 后面的是一个对象。 下面这张图说明了使用这种方式进行连接究竟发生了什么

test1有一个属性,而使用test1创建的test2要在__proto__里面才能看到test1的属性。

  • 对象和函数的继承关系: 对象的隐式原型是函数的显式原型
  • 两个对象之间的连接关系: 对象的隐式原型是另一个对象

Object.create() 的第二个参数指定了需要添加到新对象中的属性名以及描述符: