2019-12-05-学习周报

JavaScript基础

Posted by EWL on December 5, 2019

2019-12-02-本周学习周报

学习总览

JavaScript

  • this
  • 闭包

HTTP

  • http协议
    • 了解Web以及网络基础
    • 简单的HTTP协议

CSS

  • grid布局
  • CSS3新增属性以及新增选择器

HTML

  • 标签种类

学习内容

this

在描述this之前,我们可以先来回顾一下《JavaScript语言精粹》中关于this的定义以及四种使用方式:

定义

this是Javascript语言的一个关键字,它代表当前执行代码的环境对象,一般在函数内部使用。

① this的值通常是由所在函数的执行环境决定,也就是说要看函数是如何被调用的; ② 同一个函数每一次调用,this都可能指向不同的对象;

不同环境下的this

全局环境 全局环境下,无论是否是严格模式,this都指向了全局对象。

// 浏览器环境
console.log(this === window); // true

函数环境

在函数内部,this的值取决于函数被调用的方式

  1. 普通调用(基于浏览器环境) 在非严格模式下,函数调用时,其内部的this会指向window全局对象;而严格模式下,函数内部的this会指向undefined。
// 非严格模式
function foo() {
  console.log(this);
}
foo(); // Window

// 严格模式
function foo() {
  'use strict';
  console.log(this);
}
foo(); // undefined
  1. bind绑定this ES5推出了一个bind方法,用于改变当前函数的this指向,并且会返回一个与当前函数相同作用域以及函数体的函数,而且这个函数的this被永久地绑定到了bind的第一个参数对象上,无论这个函数如何调用又或者再使用其他的一些方法修改其this指向都是无效的。具体效果可以参考下面这段代码(非严格模式):
function foo() {
  return this.a;
}

foo(); // undefined

const obj1 = {
  a: 123
};

const obj2 = {
  a: 100
};

// 此时生成的bindFn01函数,其this被永久地绑定到了obj1上
const bindFn01 = foo.bind(obj1);
bindFn01(); // 123

bindFn01.call(obj2); // guess what? 返回值仍旧是123而非100
bindFn01(); // 作为window的方法调用,仍旧返回123
  1. 作为对象的方法 当函数作为对象的方法被调用时,其this会自动指向该对象。
const obj = {
  name: 'name',
  sayName: function() {
    console.log('this.name', this.name);
  }
};

obj.sayName(); // 'name'

Tips: 这样的调用行为是不受到函数声明位置或者方式的影响的,如果我们先声明函数然后再将该函数定义到对象上作为对象方法存在,我们依旧会得到相同的内容。

function foo() {
  console.log(this.name);
}

var obj = {
  name: 'name'
};

obj.f = foo;

obj.f(); // 'name'

由上述代码可以得到一个结论,函数的this指向是作为某一个函数的方法被调用,这才是决定this的关键。

此外,上面的例子都是基于普通的函数声明展开的,如果我们改成函数表达式,最终的返回结果会是什么呢?我们接下来了解一下箭头函数的this值。

  1. 箭头函数 箭头函数的this会与封闭词法环境保持一致,如果是在全局环境,则会指向全局对象,其表现用一句话形容就是箭头函数没有自己的this值,而是会继承父层环境的this。此外,箭头函数对call/apply/bind免疫,这是很重要的一个知识点,后面会给出一个笔试题做以验证。
// 全局环境
const globalThis = this;
const foo = () => this;

globalThis === foo(); // true

// 作为全局对象方法
const obj = {
  name: 'name',
  sayName: () => {
    console.log(this.name);
  }
};
obj.sayName(); // undefined

// 在函数中声明(该示例来自MDN)
// 创建一个含有bar方法的obj对象,
// bar返回一个函数,
// 这个函数返回this,
// 这个返回的函数是以箭头函数创建的,
// 所以它的this被永久绑定到了它外层函数的this。
// bar的值可以在调用中设置,这反过来又设置了返回函数的值。
var obj = {
  bar: function() {
    // -----个人评注----- 
    // this指向bar函数指向的this也就是obj对象
    var x = (() => this);
    return x;
  }
};

// 作为obj对象的一个方法来调用bar,把它的this绑定到obj。
// 将返回的函数的引用赋值给fn。
var fn = obj.bar();

// 直接调用fn而不设置this,
// 通常(即不使用箭头函数的情况)默认为全局对象
// 若在严格模式则为undefined
console.log(fn() === obj); // true

// 但是注意,如果你只是引用obj的方法,
// 而没有调用它
var fn2 = obj.bar;
// 那么调用箭头函数后,this指向Window,因为它从 bar 继承了this。
// -----个人评注----- 
// fn2是在全局环境下声明的,所以是作为Window的方法执行的,fn2的this指向Window
console.log(fn2()() == Window); // true

// -----个人评注----- 
// 将上述代码进行改写,如下
var fn2 = function () {
  var x = (() => this);
  return x;
};

// 第一次执行fn2是谁在调用fn2呢?很明显是Window全局对象,所以this指向了Window;而fn2()的返回值再次执行的时候就会打印出刚才调用fn2的对象,也就是Window
fn2()();

最后,我们来看一道笔试题:

function fun () {
    return () => {
        return () => {
            return () => {
            console.log(this.name)
                    }
                }
        }
}
var f = fun.call({name: 'foo'}); // 对普通function声明绑定成功
var t1 = f.call({name: 'bar'})()(); // 无效
var t2 = f().call({name: 'baz'})(); // 无效
var t3 = f()().call({name: 'qux'}); // 无效

// 最终结果是返回三个foo

试想,如果我们将上述代码中第一层的箭头函数改为匿名函数声明,返回结果会是什么呢?

function fun () {
    return function () {
        return () => {
            return () => {
            console.log(this.name)
                    }
                }
        }
}
var f = fun.call({name: 'foo'}); // 对普通function声明绑定成功
var t1 = f.call({name: 'bar'})()(); // bar
var t2 = f().call({name: 'baz'})(); // 'undefined'
var t3 = f()().call({name: 'qux'}); // 'undefined'

我们来分析一波,首先 fun.call({name: ‘foo’}) 会返回一个匿名函数,所以变量f实际上指向一个匿名函数体,所以对一个匿名函数使用call是可以绑定并执行成功的,所以第二行绑定到 { name: ‘bar’ } 上是会成功的,此时打印出bar;而接下来f执行的时候是作为全局变量Window的方法来执行的,所以里面几层箭头函数的this会跟着外层的f函数一起指向Window,而Window中并没有声明过一个名为name的变量,所以打印出undefined,第三行同理。 最后,我们来整理一下第二版代码:

// 省略fun声明,直接展开f/t1/t2/t3的结果
var f = function () {
  return () => {
    return () => {
      // 此处的this与最外层的匿名函数的this一致
      console.log(this.name);
    };
  };
};
f.call({ name: 'bar' }); // 函数f的this指向{ name: 'bar' }

f().call({ name: 'baz' });
// ----我是分割线---- 
// 上面这行代码分割成两部分
// 首先是f()这里我们换个写法-->window.f()
// 这下就很清晰了,函数f内部的this指向全局对象Window,所以里面的箭头函数this也会指向Window,所以最终打印name的时候,会去Window对象中寻找name属性,很明显找不到就返回undefined
// 其次是call绑定,f()返回的结果是箭头函数,绑定必然会失败,所以仍旧跟随其外层函数返打印Window中的name
  1. 原型链中的this
  2. 作为构造函数
  3. 作为一个DOM事件处理函数
  4. 作为一个内联事件处理函数

闭包

MDN对于闭包的定义是函数与对其状态即词法环境(lexical environment)的引用共同构成闭包(closure)。换成通俗的话来讲,闭包就是可以访问内部变量的函数。

首先先来看一下MDN上给出的例子:

function init() {
    var name = "Mozilla"; // name 是一个被 init 创建的局部变量
    function displayName() { // displayName() 是内部函数,一个闭包
        alert(name); // 使用了父函数中声明的变量
    }
    displayName();
}
init();

根据作用域链我们很快就能知道,作用域链的前端是displayName函数的变量对象,其中保存着对外层函数作用域的引用,所以我们在displayName中可以访问到init函数作用域中的name变量,所以在init函数中执行displayName时会循着作用域链层层向上查找,直到在init函数中找到name变量。 那,如果换成下面的写法,我们是否还能获得正确的name变量吗?

function init() {
    var name = "Mozilla"; // name 是一个被 init 创建的局部变量
    function displayName() { // displayName() 是内部函数,一个闭包
        alert(name); // 使用了父函数中声明的变量
    }
    return displayName;
}
init()();

当我们执行完init后会返回init中声明的displayName函数,而当我们再执行displayName函数时,init函数已经执行完被弹出执行栈了,按道理来讲,这时init函数中所有的变量都已经被销毁了,但是这段代码的执行结果仍然是’Mozilla’。 具体分析,我们先从理论开始:《JavaScript技术指南》中提到理论上JavaScript中所有的函数都是闭包。

理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。

实践角度:首先即使创建函数的上下文已经销毁,函数依旧存在;此外,该函数改访问了已经销毁的上下文中的变量。

所以我们来具体分析一下上面那段代码中上下文的变化状态:

  1. 进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
  2. 全局执行上下文初始化
  3. 执行 init 函数,创建 init 函数执行上下文,执行上下文被压入执行上下文栈
  4. init 执行上下文初始化,创建变量对象、作用域链、this等
  5. init执行完毕,init 执行上下文从执行上下文栈中弹出
  6. 执行 displayName 函数,创建 displayName 函数执行上下文,displayName 执行上下文被压入执行上下文栈
  7. displayName 执行上下文初始化,创建变量对象、作用域链、this等
  8. displayName 函数执行完毕,displayName 函数上下文从执行上下文栈中弹出

其中displayName创建的执行上下文中会维护一个作用域链,作用域链的前端保存这displayName函数的AO活动对象,接下来是init函数的AO活动对象,最后是全局作用域的VO变量对象。 displayName执行能alert出正确的内容,就间接说明了就算init执行完其执行上下文已经被销毁,displayName依然能根据作用域链找到对应的自由变量name。

最后,结合笔试题再来回顾一下闭包相关的知识,继续分析一波:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

我们来分析一下上面这段代码,当data[0]函数开始执行的时候,全局的变量对象是这样的:

globalVO = {
  i: 3,
  data: [
    function() { console.log(i) },
    function() { console.log(i) },
    function() { console.log(i) },
  ]
}

根据全局变量对象中的i变量的值,我们推断三个函数的打印值都是3。

如果改成闭包的写法:

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
    return function() {
      console.log(i);
    }
  })(i);
}

每一次给data设置索引以及索引值时,都是借助立即执行函数返回了一个可以访问到外层函数自由变量的函数,data[0]函数的作用域链如下:

data[0]Context = [
  AO,
  anonymousFnAO: {
    i: 0
  },
  globalVO: {
    i: 3,
    data: [
      function() { console.log(i) },
      function() { console.log(i) },
      function() { console.log(i) },
    ]
  }
]

很明显,data[0]函数的变量对象中是没有i的,但是在立即执行函数的形参中,找到了i变量,并且保存了当时的实参值。

结合规范

====================待完成====================

了解Web以及网络基础

当我们访问页面的时候,我们会在搜索栏中输入一串URL,然后根据输入的URL,web浏览器会从web服务器端获取文件资源等信息,从而显示出web页面;像这种通过发送请求获取服务器资源的web浏览器等,都可以称为客户端。这一系列的请求行为是需要受到规范制约的,web就是使用一种名为HTTP的通信协议作为规范,从而完成从客户端到服务器端的一系列运作,即web是建立在HTTP协议上通信的。

具体的TCP/IP/DNS等内容参考xmind的思维导图

====================待完成====================

简单的HTTP协议

资料参考