深入Javascript系列(持续更新)

深入Javascript系列

写在前面

寒假的时候偶然发现阿里的开发牛冴羽在Github中发布了自己的Js深入系列,看了两章之后觉得写的挺透彻的,也认识到自己对于Js的理解仅停留的能看到一些代码片,遂借此文深化自己对部分未知概念的理解,顺带总结前端学习以来自认为有趣点,算是当作笔记来摘录。

函数传值

函数参数传递包含两种方式:值传递引用传递

  • 值传递:形参是实参值的一个副本,对形参的改变不会影响实参
  • 引用传递:形参实际上是对实参引用变量的复制,导致这实参、形参都指向同一个对象实体。形参改变会同时改变实参的值。

对于Javascript来说,基本类型是值传递,(除了基本类型)例如:对象、方法,是引用传递

值传递

var aa = 1;
setNum(aa);
console.log(aa);  //1

function setNum(aaa){
  aaa = 2;
}

引用传递

var obj = {
    name:'obj',
    age:12
}
function myfunc(objtemp){
    objtemp.name='func';
    alert(objtemp.age); //12
}
myfunc(obj);
alert(obj.name);  //func

作用域&Scope Chain

见之前的文章:

深入Javascript-作用域&Scope Chain

这里补充一个点:for循环中使用var导致的变量提升问题(后文的所有专题我都会对变量提升的例子做说明,毕竟安全息息相关)

如下面的例子

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

console.log(i); //3

变量i被放入了全局变量中,此时全局上下文的VO为

globalContext = {
    VO: {
        i: 3
    }
}

前几天听了一节Google开发团队讲了一节JS开发课程,其实ES6下对作用域提升的问题已经有很好的解决方案,比如使用let

for (let i = 0; i < 3; i++) {
    console.log(i);
}

console.log(i); //3

可见拥有一个好的编程习惯对安全影响深远,例如若某个变量的值不会改变就直接用const来定义。

this

如何理解

this用途最广的就是在面向对象了。和其他语言类似,在对象内用this.xxx指代具体的属性or函数。看下面一个简单的例子,很容易就跟其他语言带入,其中这里的this指向myobj

var myobj = {
    getbar : function(){
        return this.bar;
    },
    bar: 1

};

console.log(myobj.getbar());  //1

这看起来和其他语言类似,不过JS在绑定this的时候存在变量提升的问题,你可以直接下滑看最后一个例子。

但在这之前,我们先简单了解一下JS的内存结构:

在JavaScript中生成一个具体的obj都会把地址(reference)赋值给相应的obj变量。对于上面的例子来说,变量myobj是一个地址。后面如果要读取myobj.bar,引擎先从内存拿到myobj的地址,然后再从该地址读出原始的对象,返回它的bar属性。

如果bar定义为函数的话,会返回bar所指函数的地址。

所以对于obj中的bar(无论bar是属性还是函数)来说,它的存储形式类似于下面的形式

{
  bar: {
    [[value]]: 1
    //[[value]]: 若bar为函数,此值为函数的地址
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  }
}

那么我们再来看一个例子,进一步了解this在获取值时的trick

var myobj = {
    getbar : function(){
        console.log(this.bar);
    },
    bar: 1

};

var bar = 2;
var getbar = myobj.getbar;

myobj.getbar()//1
getbar(); //2

myobjc.getbar通过myobj的地址找到getbar()函数,此时的运行环境就在myobj中,this也指向myobj。其后var getbar = myobj.getbargetbar变量指向函数本身,所以执行getbar时的this指向全局变量。

this赋值总结

上面的例子,我把它叫做this变量的范围提升(很可能是错误的叫法)。this的获取跟Reference有关,根据对Ref的操作来决定this的值是具体的内存地址还是undefined,具体的操作流程可以看mqyqingfeng的这篇文章:JavaScript深入之从ECMAScript规范解读this

回到上面的例子中执行getbar()时获取到的this实际上为undefined。非严格模式下,this的值为undefined的时候,其值会被隐式转换为全局对象

已经有前辈对this的绑定做了总结,写的很中肯,这里我贴出来:


  1. 默认的 this 绑定, 就是说在一个函数中使用了 this, 但是没有为 this 绑定对象. 这种情况下, 非严格默认(strict), this 就是全局变量 Node 环境中的 global, 浏览器(Chrome)环境中的 window.
  2. 隐式绑定: 使用 obj.foo() 这样的语法来调用函数的时候, 函数 foo 中的 this 绑定到 obj 对象.
  3. 显示绑定: foo.call(obj, …), foo.apply(obj,[…]), foo.bind(obj,…)
  4. 构造绑定: new foo() , 这种情况, 无论 foo 是否做了绑定, 都要创建一个新的对象, 然后 foo 中的 this 引用这个对象.

参考链接

JavaScript 的 this 原理

JavaScript深入之从ECMAScript规范解读this

with

with是很古老的用法,很多JS开发者已经放弃这个用法了。打比赛的时候用到了,还是讲一下的好

with的引用是方便对象的属性调用,我们看下面的例子

myobj = {
    name : 'hpdoger',
    sex : 'boy'
}

console.log(myobj.name)  //hpdoger
console.log(myobj.sex)  //boy

with(myobj){
    console.log(name)  //hpdoger
    console.log(sex) //boy
}

with 被当做重复引用同一个对象中的多个属性的”快捷方式”,可以不需要重复引用对象本身。

其次,之所以放到this的后面来讲,因为with也存在变量提升的原因。看下面的例子,其中name被提升至全局变量的范围

function foo(obj) {
    with (obj) {
        name = 'hpdoger';
    }
}

aa = {
    sex: 'boy'
}

foo(aa)
console.log(aa.name) //undefined
console.log(name)   //hpdoger

我们再来对它进行一点拓展,我们知道在Javascript中对非基本数据类型的引用是引用传递,那么也就是说我们可以通过with来返回一些意料之外的东西

比如返回Object.prototype来污染原型链

function foo(obj) {
    with (obj) {
        return __proto__
    }
}

aa = {
    name: 'boy'
}

foo(aa).name='admin'

console.log("".name) //admin

又或者是返回一个Function的构造类来RCE,参照Confidence2020-Web题解

global.flag = "flag{aaaa}";

function anonymous(a,
    /*``*/) {
    with(par)return constructor
}


function par(a) {
    console.log(123);
}

console.log(anonymous``)
console.log(anonymous`` `return flag` ``)

闭包

如何理解

定义:函数与对其状态即词法环境(lexical environment)的引用共同构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在JavaScript,函数在每次创建时生成闭包。

我们先来下面这个例子吧,探讨一下引入闭包到底要来干什么

var data = [];

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

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

在控制台打印的结果都是3,这是因为var声明的i被提升为全局作用域(如果不理解可以看之前的#Scope Chain),此时调用data[0]函数时的作用域链为

data[0]Context = {
    Scope: [AO, globalContext.VO]
}

data[0]Context 的 AO 并没有 i值,所以会从 globalContext.VO 中查找,i为 3,所以打印的结果就是 3。

同理调用data[1] 和 data[2]。

那我们用闭包的写法的来表示一下

var data = [];

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

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

现在控制台打印的结果就分别为0、1、2。因为我们在return之前加了一层匿名函数function(i)(当然你也可以加一层标准函数名来返回,不一定非要是匿名函数)。此时导致执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变如下

data[0]Context = {
    Scope: [AO, 匿名函数Context.AO globalContext.VO]
}

此时匿名函数执行上下文的AO为:

匿名函数Context = {
    AO: {
        arguments: {
            0: 0,
            length: 1
        },
        i: 0
    }
}

我们知道在 Javascript 中,如果一个对象不再被引用,那么这个对象就会被 GC 回收,否则这个对象一直会保存在内存中

如果我们以闭包的形式返回一个变量,那么封装它的上层函数中的变量就可以被记录在内存中,就像这个例子中的变量i

最后举一个我们日站中最典型的使用闭包的例子,加深对闭包的理解

(function(document){
    var viewport;
    var obj = {
        init:function(id){
           viewport = document.querySelector("#"+id);
        },
        addChild:function(child){
            viewport.appendChild(child);
        },
        removeChild:function(child){
            viewport.removeChild(child);
        }
    }
    window.jView = obj;
})(document);

(function(document){})就相当于创建了一个匿名函数,传入的参数为document

这时你可能会疑惑为什么这个匿名函数里并没有return function(xxx)这种形式,似乎不符合闭包的结构?那就是走入了对闭包这个抽象概念的误区。

我们看到在函数的最后window.jView = obj;。意思是在 window 全局对象定义了一个变量 jView,并将这个变量指向 obj 对象,即全局变量jView引用了 obj . 而 obj 对象中的函数又引用了函数 f 中的变量 viewport ,因此函数 f 中的 viewport 不会被 GC 回收,viewport 会一直保存到内存中,所以这种写法满足了闭包的条件。

总结

当我们需要在模块中定义一些变量,并希望这些变量一直保存在内存中但又不会“污染”全局的变量时,就可以用闭包来定义这个模块

not found!