用生命谱写代码的赞歌

0%

JavaScript 知识点

JS 知识点

本文用于记录 JS 中一些容易忘记、遗漏、混淆的知识点,不定时更新……

JS 语法

  1. 回调函数:把一个方法A当一个参数值传递到另外一个函数B中,在B执行的过程当中我们随时根据需求让A方法执行;

    • 什么是回调 :它是异步编程基本的方法,需要异步处理的时候一般采用后续传递的方式,将后续逻辑作为起始函数的参数。
    • PS: 典型的异步方法有:setTimeout,回调函数,ajax,事件
  2. for循环

    1
    2
    3
    4
    5
    var a = ['a', 'b', 'c', 'd', 'e']
    // 内部表达式排列顺序不能混淆
    for(var i = 0; i < a.length; i++) {
    console.log(a[i])
    }
  3. js 关键字in的用法

    • for…in迭代
    • for…in用于数组的循环迭代操作,迭代出来的是数组的索引
    • for…in用于对象的循环迭代操作,迭代出来的是对象的属性
    • 判断对象是否为数组/对象的元素/属性:**_(变量 in 对象)_**
      • 当“对象”为数组时,“变量”指的是数组的“索引”
      • 当“对象”为对象是,“变量”指的是对象的“属性”无论属性是存在于实例中还是原型中
  4. JS 中 == 的用法

    依据jquery的源码,程序中所有的等于判断都要用===

    除了 obj.c == null 这种情况,需要同时兼顾 obj.c === undefined 和 obj.c === null

  5. JS 中 if (变量) 与 if (变量 === true)

    在 Java 中 if (条件) {} 括号内的条件是一个布尔值,而在 JS 中 if (变量),这个变量只有满足:

    变量如果不为 “”,0,null,undefined,false,都会被处理为 false,反之为 true。只要变量有非0的值或是某个对象,数组,字符串,都会认为 true。

  6. js 中 delete 运算符(MDN)

    delete object.property

    或者 delete object['property'],对于所有情况都是 true ,除非属性是一个自己的不可配置属性,在这种情况下,非严格模式返回 false 。

    当删除一个数组元素时,被删除的元素已经完全不属于该数组,但数组的 length 属性并不会变小

node 的 CommonJS 规范

根据这个规范,每个文件就是一个模块,有自己的作用域,文件中的变量、函数、类等都是对其他文件不可见的。

如果想在多个文件分享变量,必须定义为global对象的属性。(不推荐)

module.exports 与 exports的区别

每一个node.js执行文件,都自动创建一个module对象,同时,module对象会创建一个叫exports的属性,初始化的值是 {}

1
module.exports = {};

Node.js为了方便地导出功能函数,node.js会自动地实现以下这个语句:

foo.js

1
2
3
4
exports.a = function(){
console.log('a')
}
exports.a = 1

test.js

1
2
var x = require('./foo');
console.log(x.a)

从上面可以看出,exports是引用 module.exports的值。module.exports 被改变的时候,exports不会被改变,而模块导出的时候,真正导出的执行是module.exports,而不是exports。总结如下:

  1. module.exports 初始值为一个空对象 {}
  2. exports 是指向的 module.exports 的引用
  3. require() 返回的是 module.exports 而不是 exports

module.parent 指什么

  • The module that first required this one.
  • 谁第一个require他 parent就是谁

JS 属性类型

  1. 数据属性(有4个描述其行为的特性)

    1. [[Configurable]]: 表示能否通过 delete 删除属性,能否修改属性的特性,或者能否修改为访问器属性
    2. [[Enumerable]]: 表示能否通过 for-in 循环返回属性
    3. [[Writable]]: 表示能否修改属性的值
    4. [[Value]]: 包含这个属性的数据值,这个特性默认值为 undefined

    如果直接在对象上定义属性,前三个特性的默认值为 true 。 在调用 Object.defineProperty(Obj, 'name', {configurable: true, value: 'xxx'}) 方法时,如果不指定,configurable、enumerable和 writable 特性的默认值均为 false。

  2. 访问器属性:不包含数据值,包含一对儿 gettersetter 函数(不过,这两个函数不是必需的)。

    1. [[Configurable]]: 表示能否通过 delete 删除属性,能否修改属性的特性,或者能否修改为访问器属性
    2. [[Enumerable]]: 表示能否通过 for-in 循环返回属性
    3. [[Get]]: 在读取属性时调用的函数。默认值为 undefined
    4. [[Set]]: 在写入属性时调用的函数。默认值为 undefined

    访问器属性不能直接定义,必需使用 Object.defineProperty(book, 'year', {get: function() {return this._year}, set: function(newValue) {this._year = newValue; this.edition += newValue - 2004}}) 来定义。

JS 实例属性搜索原则

当访问一个实例属性时,首先会在实例中搜索该属性。如果没有找到该属性,则会继续搜索实例的原型(__proto__)。在通过原型链实现继承的情况下,搜索过程得以沿着原型链继续向上,直到原型链的末端。

监听事件的节流思想

当页面监听“scroll”、“size”等事件时,如果用到setTimeoutsetInterval等定时器,需要在开始是清空定时器,防止多次重复监听事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let timeoutId
function callback() {
const top = div.getBoundingClientRect().top
const windowHeight = window.screen.height
if (top && top < windowHeight) {
loadMoreFn()
}
}
window.addEventListener('scroll', function () {
if (timeoutId) {
clearTimeout(timeoutId)
}
timeoutId = setTimeout(callback, 50)
}, false)

JS 执行过程

作用域提升

  1. 提升

    函数声明和变量声明都会被提升。但首先是函数声明被提升,然后才是变量,重复的声明会被忽略。如果变量名与函数名相同,以函数名优先

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    console.log(foo) // function foo() { console.log(1) }
    var foo = 3

    function foo() {
    console.log(1)
    }
    console.log(foo) // 3

    foo = function() {
    console.log(2)
    }
    console.log(foo) // function () { console.log(2) }

    console.log(a)
    a = 10 // 报错,只有var声明的变量才能提升
  2. 变量重名

    1. 形参重名

      在函数中形参的初始值是最右边的形参对应的实参值,如果实参不存在则为undefined

      1
      2
      3
      4
      5
      6
      function fn(a,a,a) {
      console(a);
      }
      fn(1,2,3); // 输出 3
      fn(1,2); // 输出 undefined
      fn(1); // 输出 undefined
    2. 函数与形参重名

      函数与形参重名时,变量的初始值是函数

      1
      2
      3
      4
      5
      function fn(a) {
      function a() {}
      console.log(typeof a); // function
      }
      fn(1);
    3. 形参是 arguments

      此时arguments指的是函数中那个特殊变量arguments,如果一个参数是arguments,那么arguments的初始值对应传过来的实参

      1
      2
      3
      4
      function fn(arguments) {
      console.log(arguments); // 1
      }
      fn(1);
    4. 在变量中声明一个 arguments

      重新声明的arguments无效,arguments的初始值是在JS中定义的那个

      1
      2
      3
      4
      5
      function fn() {
      var arguments;
      console.log(typeof arguments); // object
      }
      fn();

    上述测试表明,重名时初始值的优先级顺序是:函数声明 > 形参,形参 > arguments,arguments > 变量声明。当然,变量赋值又是另一回事了,它能改变传过来的实参值。

  3. 块语句

    如 if 和 switch 条件语句或 for 和 while 循环语句,不像函数,它们不会创建一个新的作用域。在块语句中定义的变量将保留在它们已经存在的作用域中。

    1
    2
    3
    4
    5
    if (true) {
    // 'if' 条件语句块不会创建一个新的作用域
    var name = 'Hammad'; // name 依然在全局作用域中
    }
    console.log(name); // logs 'Hammad'

    ECMAScript 6 引入了 let 和 const 关键字。与 var 关键字相反,let 和 const 关键字支持在局部(本地)作用域的块语句中声明。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    if (true) {
    // 'if' 条件语句块不会创建一个新的作用域
    // name 在全局作用域中,因为通过 'var' 关键字定义
    var name = 'Hammad';
    // likes 在局部(本地)作用域中,因为通过 'let' 关键字定义
    let likes = 'Coding';
    // skills 在局部(本地)作用域中,因为通过 'const' 关键字定义
    const skills = 'JavaScript and PHP';
    }
    console.log(name); // logs 'Hammad'
    console.log(likes); // Uncaught ReferenceError: likes is not defined
    console.log(skills); // Uncaught ReferenceError: skills is not defined

函数执行环境

  1. 执行上下文(Execution Context)
    • 当前执行上下文分为变量对象VO,作用域链ScopeChain,以及this
    • 当调用一个函数时(激活),一个新的执行上下文就会被创建。而一个执行上下文的生命周期可以分为两个阶段。
      1. 创建阶段:在这个阶段中,执行上下文会分别创建变量对象,建立作用域链,以及确定this的指向
      2. 代码执行阶段:创建完成之后,就会开始执行代码,这个时候,会完成变量赋值,函数引用,以及执行其他代码。
  2. 变量对象(Variable Object)
    • 变量对象创建过程
      1. 建立arguments对象。检查当前上下文中的参数,建立该对象下的属性与属性值。
      2. 先检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。
      3. 再检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。

当处于执行上下文创建阶段时,这时候还没有执行代码

变量对象会包括:

  1. 函数的所有形参 (如果是函数上下文)
    • 由名称和对应值组成的一个变量对象的属性被创建
    • 没有实参,属性值设为 undefined
    • 函数声明
  2. 函数声明
    • 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
    • 如果变量对象已经存在相同名称的属性,则完全替换这个属性
    • 变量声明
  3. 变量声明
    • 由名称和对应值(undefined)组成一个变量对象的属性被创建;
    • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

从上面规则可以看出,function声明会比var声明优先级更高一点。下面举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
// demo01
function test() {
console.log(a);
console.log(foo());

var a = 1;
function foo() {
return 2;
}
}

test();

在上例中,我们直接从test()的执行上下文开始理解。全局作用域中运行test()时,test()的执行上下文开始创建。为了便于理解,我们用如下的形式来表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建过程
testEC = {
// 变量对象
VO: {},
scopeChain: {},
this: {}
}

// VO 为 Variable Object的缩写,即变量对象
VO = {
arguments: {...}, //注:在浏览器的展示中,函数的参数可能并不是放在arguments对象中,这里为了方便理解,我做了这样的处理
foo: '<foo reference>', // 表示foo的地址引用
a: undefined
}

未进入执行阶段之前,变量对象中的属性都不能访问!但是进入执行阶段之后,变量对象转变为了活动对象,里面的属性都能被访问了,然后开始进行执行阶段的操作。

这样,如果再面试的时候被问到变量对象和活动对象有什么区别,就又可以自如的应答了,他们其实都是同一个对象,只是处于执行上下文的不同生命周期。

1
2
3
4
5
6
7
// 执行阶段
VO -> AO // Active Object
AO = {
arguments: {...},
foo: '<foo reference>',
a: 1
}

因此,上面的例子demo1,执行顺序就变成了这样

1
2
3
4
5
6
7
8
9
10
11
12
function test() {
function foo() {
return 2;
}
var a;
console.log(a);
console.log(foo());
a = 1;
}

test();

再来一个例子,巩固一下我们的理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// demo2
function test() {
console.log(foo);
console.log(bar);

var foo = 'Hello';
console.log(foo);
var bar = function () {
return 'world';
}

function foo() {
return 'hello';
}
}

test();

// 创建阶段
VO = {
arguments: {...},
foo: '<foo reference>',
bar: undefined
}
// 这里有一个需要注意的地方,因为var声明的变量当遇到同名的属性时,会跳过而不会覆盖

// 执行阶段
VO -> AO
VO = {
arguments: {...},
foo: 'Hello',
bar: <bar reference>
}

函数执行过程

  • 构造函数的执行过程
    1. 先创建一个空对象
    2. 将构造函数的作用域交给上述对象,this => 空对象
    3. 开始执行构造函数内部代码
    4. return this (默认)

注意: 在构造函数中,如果显示返回一个基本数据类型的数据,会被忽略掉,当然返回对象就不会

  • 立即执行函数
    1. 编写一个 js 库通常会通过立即执行函数包裹自己的业务逻辑,主要有以下目的:
    2. 避免全局污染: 所有库的逻辑以及定义和使用的变量全部被封装到该函数的作用域中
    3. 隐私保护: 但凡立即执行函数中声明的函数/变量等,除非自己想暴露,否则无法再外部获得
    4. 函数对象的length属性,揭示了函数的形参个数
1
2
function add(a,b) {return a+b;}
console.log(add.length;) // 2

函数调用时实参和形参之间的值如何传递

通过函数传递的参数是局部变量

  • 按值传递(call by value)
    • 函数的形参是被调用时所传实参的副本。修改形参的值并不会影响实参。

JS的基本类型,是按值传递的。

1
2
3
4
5
6
var a = 1;
function foo(x) {
x = 2;
}
foo(a);
console.log(a); // 仍为1, 未受x = 2赋值所影响
  • 按引用传递(call by reference)
    • 函数的形参接收实参的隐式引用,而不再是副本。这意味着函数形参的值如果被修改,实参也会被修改。同时两者指向相同的值。
  • 按共享传递(call by sharing)
    • JS中的基本类型按值传递,对象类型按共享传递的(call by sharing,也叫按对象传递、按对象共享传递)。
    • 调用函数传参时,函数接受对象实参引用的副本(既不是按值传递的对象副本,也不是按引用传递的隐式引用)。
    • 它和按引用传递的不同在于:在共享传递中对函数形参的赋值,不会影响实参的值。

如下面例子中,不可以通过修改形参o的值,来修改obj的值。

1
2
3
4
5
6
var obj = {x : 1};
function foo(o) {
o = 100;
}
foo(obj);
console.log(obj.x); // 仍然是1, obj并未被修改为100.

然而,虽然引用是副本,引用的对象是相同的。它们共享相同的对象,所以修改形参对象的属性值,也会影响到实参的属性值。

1
2
3
4
5
6
var obj = {x : 1};
function foo(o) {
o.x = 3;
}
foo(obj);
console.log(obj.x); // 3, 被修改了!

对于对象类型,由于对象是可变(mutable)的,修改对象本身会影响到共享这个对象的引用和引用副本。而对于基本类型,由于它们都是不可变的(immutable),按共享传递与按值传递(call by value)没有任何区别,所以说JS基本类型既符合按值传递,也符合按共享传递。

1
var a = 1; // 1是number类型,不可变 var b = a; b = 6;

据按共享传递的求值策略,a和b是两个不同的引用(b是a的引用副本),但引用相同的值。由于这里的基本类型数字1不可变,所以这里说按值传递、按共享传递没有任何区别。

  1. 函数作为参数被引用(引用函数不能传参)

    1. 我们不能给函数的引用传递参数,就像下面这样

      1
      2
      3
      4
      5
      // works
      nav.addEventListener('click', toggleNav, false);

      // will invoke the function immediately
      nav.addEventListener('click', toggleNav(arg1, arg2), false);
    2. 我们可以解决这个问题,通过在它里面创建一个新的函数:

      1
      2
      3
      nav.addEventListener('click', function () {
      toggleNav(arg1, arg2);
      }, false);

      但是这样就改变了作用域,我们又一次创建了一个不需要的函数,这样做需要花费很多,当我们在一个循环中绑定事件监听的时候.

    3. 这时候就需要.bind()闪亮登场了,因为我们可以使用他来进行绑定作用域,传递参数,并且函数还不会立即执行:

      1
      nav.addEventListener('click', toggleNav.bind(scope, arg1, arg2), false);

闭包(closure)

闭包是指有权访问另外一个函数作用域中的变量的函数

  • 闭包创造条件
    1. 存在内、外两层函数
    2. 内层函数对外层函数的局部变量进行了引用

闭包的作用是“保留住”了局部变量,使内层函数调用时,可以重复使用该变量;而不同于全局变量,该变量只能在函数内部被引用。
每当你看到一个函数里面存在着另一个函数,那么内部的函数能够访问外部函数的作用域,这就叫做词法作用域或者闭包,也被认为是静态作用域。

Ajax

  • 跨域(安全策略,浏览器为了安全单方面限制跨域)
    1. 纯前端jsonp(postMessage)
    2. 借助服务端
      • 通过发送Ajax请求访问自己写的php页面获取数据,php页面请求跨域页面的内容然后返还给Ajax请求,php运行在服务器端不受跨域影响
      • 服务器端设置请求头CORS
    3. 借助服务器软件 apache/nginx(反向代理)
    4. 浏览器插件

div 的 click 事件在手机浏览器不生效问题

使用 jQuery 给 div 绑定 click 事件, 在 PC 端浏览器可以正常触发, 但是在手机浏览器上无法触发

解决办法:

  1. 给 div 添加 cursor: pointer 属性即可

  2. click 改为 touchstart 事件, 或者共存. 但是 touchstart 在滑动页面的时候会误触, 推荐使用 click + cursor

    1
    2
    3
    $(document).on("click touchstart", ".name", function() {
    alert('name');
    });
  3. 把事件添加到 a, button 等本身可以点击的标签上, 确保 click 正常使用