JavaScript难点


  • 原型和原型链
  • 作用域和闭包
  • 异步和单线程
  • this指针

1.原型和原型链

JavaScript中原型和原型链是其面向对象编程的基础,即时ES6中关键字class实际上也是基于原型链和闭包设置的一个语法糖,本质上JavaScript中面向对象就是一个拓展对象原型链的过程。

1.1数据类型

JavaScript中数据类型主要分为两种类型,第一种为原始类型,第二种为对象(引用)类型。

对于原始类型来说,可以直接通过typeof确定变量原始数据类型,但对于任一Object派生出的结构类型使用typeof将总会得到”object”。检查Object类型最适合但结果不完全正确的方法为 instanceof。

typeof undefined; //undefined 有效
typeof true; //boolean 有效
typeof 123; // number 有效
typeof "Helloworld";//string 有效
typeof null; //object 无效

typeof [] ; //object 无效
typeof new Date(); //object 无效
typeof new Function(); // function 有效

1.1.1原始类型

在JavaScript中,原始类型的值时无法被更改的。例如有某一字符串值为”abc”,通过操作追加字符”d”后得到新字符串,实际上原字符串并没有被改变,而是JS新建一字符串并返回。引用MDN示例:

// 使用字符串方法不会改变一个字符串
var bar = "baz";
console.log(bar);               // baz
bar.toUpperCase();
console.log(bar);               // baz

// 使用数组方法可以改变一个数组
var foo = [];
console.log(foo);               // []
foo.push("plugh");
console.log(foo);               // ["plugh"]

// 赋值行为可以给基本类型一个新值,而不是改变它
bar = bar.toUpperCase();       // BAZ

原始类型在函数传参过程中都是值传递,即直接将参数值复制一份压入栈中供函数获取,因此函数中对参数操作实际不会改变函数外部原始类型变量值。

let foo = 1;
let bar = 1;
function fn1(foo){foo += 1;}
function fn2(a){a += 1; foo += 1;}

fn1(foo);
console.log(`foo:${foo}`) //foo:1   <1

fn2(foo)
console.log(`foo:${foo}`) //foo:2   <2

fn2(bar)
console.log(`bar:${bar}`) //bar:1   <3
console.log(`foo:${foo}`) //foo:3   <4

<1 处foo不改变的原因为函数内部词法作用域覆盖了外部foo。因此fn1函数中无法通过任何方法访问外部foo,而只能访问本地参数foo(栈)。<2、<4处则可以直接访问到外部foo,因此foo值发生变化。该知识点与闭包有联系。

<3 处可以看出fn2(bar)传入bar并在函数内部改变新参值,而外部bar变量并未发生变化,因此函数传参方式为值传递。

原始类型number、string、boolean都有相对应的包装类型。而从包装类型通过valueOf()获取相应基本类型值。

1.1.2对象类型

对象类型实际上是一种引用类型,即程序变量中持有的数据为堆中对象实际存储的起始地址。类似C语言中指针类型。

因此若对对象类型施加const限制,实际上是无法限制对象中属性值的变更,而是单纯限制对象变量中存储地址值更改和重申明。

由于引用类型的特殊性,在此引入了关于对象深浅拷贝的问题。例如将一对象变量赋值给另一变量实际上是将对象内存地址复制一份给新变量,因此两变量都是指向了同一块内存地址。所以无论操作哪个变量,另一变量都会响应变化(因为就是一样的)。

1.2原型与原型链

JavaScript原型及原型链特性从另一个方向为Javascript提供了实现继承的方案。

对于JavaScript来说,每一个实例对象(object)都有一个指向其构造函数prototype属性的私有属性__proto__,当通过’ . ‘操作符访问某一对象属性时,若对象本身不存在该属性,则会从对象__proto__属性一层一层向上寻找直到找到第一个与属性名匹配的属性,返回其值。而匹配搜寻的终点是第一个原型属性为null的对象(由于null没有原型,因此作为原型链最后一个环节)。

ps: undefined代表定义未赋值,null代表定义并赋值,只是值为null。

几乎所有JavaScript对象都是位于原型链顶端的Object的派生类,而Object数据类型的__proto__为null,构成了内置对象类型整个原型链。

//构造函数
let Foo = function(){
this.a = 1;
this.b = 2;
}
//扩展原型
Foo.prototype.b = 3;
Foo.prototype.c = 4;

let f1 = new Foo();
//  f1 -->  {a:1,b:2} --f1.__proto__-->Foo.prototype:{b:3,c:4} \
//--Foo.prototype.__proto__-->Object.prototype:{hasOwnProperty:function(){...},.....} \
//--Object.prototype.__proto__-->null 
console.log(f1.a);//1
console.log(f1.b);//2 property shadowing优先对象属性,没找到再找原型链上层
console.log(f1.c);//4 原型链上f1.__proto__== Foo.prototype,因此从Foo原型属性找到c属性
console.log(f1.hasOwnProperty)//function hasOwnProperty(){..} 从原型链上f1.__proto__.__proto___ = Object.prototype找到该属性

通过图来理解一下:

1.2.1创建对象和生成原型链的不同方法

使用构造函数创建的方法

通过创建构造函数并使用new关键字来创建对象的时候这个方法被称为构造方法。

function Student(){
this.name = '';
this.age = 18;
}
Student.prototype = {
   setAge:function(vage){
this.age = vage;
   }
}
var stu = new Student();
//此时stu.__proto__指向Student.prototype
stu.setAge(20);
console.log(stu.age)// 20
使用Object.create函数创建对象
Object.create(proto,[propertiesObject]) //Object.create()方法用于创建一个拥有特定原型的新对象。
var stuProto = { 
name:'Xiaoming',
age:18,
setAge:function(vage){this.age = vage}
};
var stu1 = Object.create(stuProto);
console.log(`student1.name=${stu1.name},student1.age=${stu1.age}`);// Xiaoming 18
stu1.setAge(20);
console.log(`student1.age=${stu1.age}`);// 20
class关键字创建对象

class是ES6引入用于方便实现面向对象编程的关键字,实际上它也是JavaScript原型实现类的一种语法糖,JavaScript仍然是使用原型实现类和类继承。

"use strict";

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

class Square extends Polygon {
  constructor(sideLength) {
    super(sideLength, sideLength);
  }
  get area() {
    return this.height * this.width;
  }
  set sideLength(newLength) {
    this.height = newLength;
    this.width = newLength;
  }
}

var square = new Square(2);

1.2.2拓展原型链的方法

详见MDN 总结:4个用于拓展原型链的方法


2.作用域和闭包

MDN上对于闭包的定义:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

闭包的本质为:词法作用域和函数当值传递

2.1变量作用域

var、let、const从作用域上来var和后两者有一定区别。

直接声明的变量:如x = 1;该变量为隐式地创建为全局变量,即成为全局对象的属性。

var关键字

var声明的变量作用域范围是它当前执行上下文,执行上下文可以是嵌套的函数。如果声明在函数之外则为全局变量。即作用域为当前函数内部。

var variable = 1;

function fn1(){

var inner = 1;
var variable= 2;
global_variable = 3;
console.log(variable);//2 局部变量variable作用域覆盖全局变量variable
console.log(inner);//1
function b(){
console.log(variable); //2 作用域内嵌套函数也可以访问
console.log(inner);  // 1
}
b();
}
fn1();
console.log(variable); //1 外部variable作用域,var声明变量作用域为执行上下文即函数内部。
console.log(inner); //inner is not defined
console.log(global_variable);//未加关键字声明变量为全局变量

let关键字

作用域为当前所在代码块内部(块级作用域),无法重复声明,存在变量提升。let声明的变量直到它们被执行时才进行初始化,因此存在暂时性死区问题。

let x= 1;
{
  let in_block = 2;
  console.log(in_block)//2
}
console.log(x);//1
console.log(in_block);//in_block is not defined

function fn(){
let in_func= 3;
let x = 4;
{
let in_func_block = 5;
}
console.log(in_func);//3
console.log(x);//4 作用域覆盖
console.log(in_func_block );//in_func_block is not defined
}
fn();
console.log(in_func);//in_func is not defined
console.log(x);//1

let dup = 1;
let dup = 2;//Identifier 'dup' has already been declared

let暂存死区:作用域中若let声明变量为进行初始化之前使用该变量将抛出ReferenceError错误。

function fn(){
console.log(foo);//undefined
console.log(bar);//ReferenceError let声明的变量在未初始化前使用将抛出ReferenceError的错误
var foo = 1;
let bar = 1;
}
fn();

//----------------------exp2

function test(){
   var foo = 33;
   if (foo) {
      let foo = (foo + 55); // ReferenceError if块内foo被认为是该if块词法作用域内部let声明的foo变量,因此抛出错误
   }
}
test();

const关键字:

作用域与let关键字相同 ,无法重复声明,存在变量提升,同样拥有暂存死区,但不同的是const声明的常量需要赋初值。

2.2 提升(Hoisting)

引用MDN:JavaScript Hoisting refers to the process whereby the interpreter appears to move the declaration of functions, variables or classes to the top of their scope, prior to execution of the code.

因此Hoisting提升的对象包括函数、变量、类,而提升位置为他们各自作用域的顶部,提升时间为执行代码之前。

函数提升:

Hoisting特性允许函数在被声明前可以使用:

say("Hello World!");//Hello World!

function say(statement){
console.log(statement);
}

变量提升:

变量也会被提升,但变量仅会被提升声明而不是初始化,因此在变量初始化语句之前实用变量有一定风险。

这里var与let、const有所不同,var声明的变量在变量提升后默认值为undefined,而let、const则不会进行初始化,在实际初始化前使用let、const声明变量会导致ReferenceError抛出,作用域顶部到let、const实际初始化之间这段区域被称为暂存死区

console.log(foo);//1
console.log(bar);//Cannot access 'bar' before initialization
console.log(con);//Cannot access 'con' before initialization

var foo = 1;
let bar = 2;
const con = 3;

2.3 闭包(Closures)


v0.3wep 新建文章,写到闭包

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注