js进阶问题

269 阅读9分钟
进阶问题
1.let var function

变量的赋值可以分为三个阶段:

1.创建变量,在内存中开辟空间 2.初始化变量,将变量初始化为undefined 3.真正赋值 关于let , var , 和function:

  • let的创建过程被提升了,但是初始化没有提升
  • var的创建和初始化都被提升了
  • function的创建,初始化和赋值都被提升了
let name = 'ConardLi'
{
  console.log(name) // Uncaught ReferenceError: name is not defined
  let name = 'code秘密花园'
}

let变量如果不存在变量提升,console.log(name)就会输出ConardLi,结果却抛出了ReferenceError,那么这很好的说明了,let也存在变量提升,但是它存在一个“暂时死区”,在变量未初始化或赋值前不允许访问。

2.let和var
for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 1);
    console.log(i);
 }
  
for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 1);
    console.log(i);
 }
 // 0 1 2 0 1 2 3 3 3 0 1 2 
3.普通函数和箭头函数
const shape = {
  radius: 10,
  diameter() {
    return this.radius * 2;
  },
  perimeter: () => 2 * Math.PI * this.radius
};

shape.diameter();// 20
shape.perimeter();// NaN

请注意,diameter是普通函数,而perimeter是箭头函数。对于箭头函数,this关键字指向是它所在上下文(定义时的位置)的环境,与普通函数不同! 这意味着当我们调用perimeter时,它不是指向shape对象,而是指其定义时的环境(window)。没有值radius属性,返回undefined。

4.符号+和符号!
+true; // 1
!"Lydia"; // false

一元加号会尝试将boolean类型转换为数字类型。 true被转换为1,false被转换为0。字符串'Lydia'是一个真值。 我们实际上要问的是“这个真值是假的吗?”。 这会返回false。

5.静态方法
class Chameleon {
  static colorChange(newColor) {
    this.newColor = newColor;
  }

  constructor({ newColor = "green" } = {}) {
    this.newColor = newColor;
  }
}

const freddie = new Chameleon({ newColor: "purple" });
freddie.colorChange("orange"); // TypeError

colorChange方法是静态的。 静态方法仅在创建它们的构造函数中存在,并且不能传递给任何子级。 由于freddie是一个子级对象,函数不会传递,所以在freddie实例上不存在freddie方法:抛出TypeError。

6.对象也是方法
function bark() {
  console.log("Woof!");
}

bark.animal = "dog";// 不报错
7.不能像使用常规对象那样向构造函数添加属性
function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

const member = new Person("Lydia", "Hallie");
Person.getFullName = () => this.firstName + this.lastName;

console.log(member.getFullName());// TypeError
8.new关键字
function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

const lydia = new Person("Lydia", "Hallie");
const sarah = Person("Sarah", "Smith");

console.log(lydia);// Person {firstName: "Lydia", lastName: "Hallie"} 
console.log(sarah);// undefined

对于sarah,我们没有使用new关键字。 使用new时,它指的是我们创建的新空对象。 但是,如果你不添加new它指的是全局对象! 我们指定了this.firstName等于'Sarah和this.lastName等于Smith。 我们实际做的是定义global.firstName ='Sarah'和global.lastName ='Smith。 sarah本身的返回值是undefined。

9.事件传播的三个阶段

捕获 > 目标 > 冒泡

在捕获阶段,事件通过父元素向下传递到目标元素。 然后它到达目标元素,冒泡开始。

10.对象原型

除基础对象外,所有对象都有原型。 基础对象可以访问某些方法和属性,例如.toString。 这就是您可以使用内置JavaScript方法的原因! 所有这些方法都可以在原型上找到。 虽然JavaScript无法直接在您的对象上找到它,但它会沿着原型链向下寻找并在那里找到它,这使您可以访问它。

译者注:基础对象指原型链终点的对象。基础对象的原型是null。

11.模板字符串
function getPersonInfo(one, two, three) {
  console.log(one);// ["", "is", "years old"]
  console.log(two);// Lydia
  console.log(three);// 21
}

const person = "Lydia";
const age = 21;

如果使用标记的模板字符串,则第一个参数的值始终是字符串值的数组。其余参数获取传递到模板字符串中的表达式的值!

12.对象的比较
function checkAge(data) {
  if (data === { age: 18 }) {
    console.log("You are an adult!");
  } else if (data == { age: 18 }) {
    console.log("You are still an adult.");
  } else {
    console.log(`Hmm.. You don't have an age I guess`);
  }
}
// Hmm.. You don't have an age I guess

在比较相等性,原始类型通过它们的值进行比较,而对象通过它们的引用进行比较。JavaScript检查对象是否具有对内存中相同位置的引用。 我们作为参数传递的对象和我们用于检查相等性的对象在内存中位于不同位置,所以它们的引用是不同的。 这就是为什么{ age: 18 } === { age: 18 }和 { age: 18 } == { age: 18 } 返回 false的原因。

13.原型类
String.prototype.giveLydiaPizza = () => {
  return "Just give Lydia pizza already!";
};

const name = "Lydia";

name.giveLydiaPizza();// "Just give Lydia pizza already!"

String是一个内置的构造函数,我们可以为它添加属性。 我刚给它的原型添加了一个方法。 原始类型的字符串自动转换为字符串对象,由字符串原型函数生成。 因此,所有字符串(字符串对象)都可以访问该方法!

译者注: 当使用基本类型的字符串调用giveLydiaPizza时,实际上发生了下面的过程:

  • 创建一个String的包装类型实例
  • 在实例上调用substring方法
  • 销毁实例
14.JavaScript只有原始类型和对象

原始类型是boolean,null,undefined,bigint,number,string和symbol。

15.做幂运算时用幂操作符 **
// bad
const binary = Math.pow(2, 10);

// good
const binary = 2 ** 10;
16.用const或let声明变量。不这样做会导致全局变量。 我们想要避免污染全局命名空间
// bad
superPower = new SuperPower();

// good
const superPower = new SuperPower();
17.不要使用链接变量分配
// bad
(function example() {
  // JavaScript 将这一段解释为
  // let a = ( b = ( c = 1 ) );
  // let 只对变量 a 起作用; 变量 b 和 c 都变成了全局变量
  let a = b = c = 1;
}());

console.log(a); // undefined
console.log(b); // 1
console.log(c); // 1

// good
(function example() {
  let a = 1;
  let b = a;
  let c = a;
}());

console.log(a); // undefined
console.log(b); // undefined
console.log(c); // undefined

// `const` 也是如此
18.不要使用一元自增自减运算符(++, --)

Why? 根据eslint文档,一元增量和减量语句受到自动分号插入的影响,并且可能会导致应用程序中的值递增或递减的无声错误。 使用num + = 1而不是num ++或num ++语句来表达你的值也是更有表现力的。 禁止一元增量和减量语句还会阻止您无意地预增/预减值,这也会导致程序出现意外行为。

19.空格问题

1.作为语句的花括号内也要加空格 —— { 后和 } 前都需要空格。 2., 前不要空格, , 后需要空格。 3.在对象的字面量属性中, key value 之间要有空格。

// bad
var obj = { "foo" : 42 };
var obj2 = { "foo":42 };

// good
var obj = { "foo": 42 };
20.闭包
function A(){
    function B(){
       console.log('Hello Closure!');
    }
    return B;
}
var C = A();
C();// Hello Closure!
  • 定义普通函数 A
  • 在 A 中定义普通函数 B
  • 在 A 中返回 B
  • 执行 A,并把 A 的返回结果赋值给变量 C
  • 执行 C

把这5步操作总结成一句话就是:

函数A的内部函数B被函数A外的一个变量 c 引用。 把这句话再加工一下就变成了闭包的定义: 当一个内部函数被其外部函数之外的变量引用时,就形成了一个闭包。 因此,当你执行上述5步操作时,就已经定义了一个闭包! 这就是闭包。

21.闭包的用途

在了解闭包的作用之前,我们先了解一下 Javascript 中的 GC 机制:在 Javascript中,如果一个对象不再被引用,那么这个对象就会被 GC 回收,否则这个对象一直会保存在内存中。

在上述例子中,B 定义在 A 中,因此 B 依赖于 A ,而外部变量 C 又引用了 B , 所以A间接的被 C 引用。 也就是说,A 不会被 GC 回收,会一直保存在内存中。为了证明我们的推理,上面的例子稍作改进:

function A() {
    var count = 0;
    function B() {
       count ++;
       console.log(count);
    }
    return B;
}
var C = A();
C();// 1
C();// 2
C();// 3

count 是函数A 中的一个变量,它的值在函数B 中被改变,函数 B 每执行一次,count 的值就在原来的基础上累加 1 。因此,函数A中的 count 变量会一直保存在内存中。 当我们需要在模块中定义一些变量,并希望这些变量一直保存在内存中但又不会 “污染” 全局的变量时,就可以用闭包来定义这个模块。

22.闭包的高级写法

上面的写法其实是最原始的写法,而在实际应用中,会将闭包和匿名函数联系在一起使用。下面就是一个闭包常用的写法:

(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(){}) 和 () 。

第1个() 是一个表达式,而这个表达式本身是一个匿名函数,所以在这个表达式后面加 () 就表示执行这个匿名函数。

因此这段代码执行执行过程可以分解如下:

var f = 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;
};
f(document);

在这段代码中似乎看到了闭包的影子,但 f 中没有任何返回值,似乎不具备闭包的条件,注意这句代码:

window.jView = obj;

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

23.js文件的引入方式
<script type="text/javascript" src="./slice.js"></script>