「JavaScript灵魂之问」前端知识梳理之 JS 篇(中秋特别篇)

JS 错误信息类型

语法错误

1、SyntaxError 语法错误

// 变量名不规范
var 1 = 1;

SyntaxError: Unexpected number 意外的数字

var 1ab = 1;

SyntaxError: Invalid or unexpected token 其中token指代标记的意思。

// 关键字不可赋值
new = 5;

SyntaxError: Unexpected token '='

// 基本语法错误
var a = 5:

SyntaxError: Unexpected token ':'

引用错误

2、ReferenceError 引用错误、

// 变量或函数未被声明
test();

ReferenceError: test is not defined

console.log(a);

ReferenceError: a is not defined

// 给无法被赋值的对象赋值的时候
var a = 1 = 2;

SyntaxError: Invalid left-hand side in assignment

范围错误

3、RangeError 范围错误

// 数组长度赋值为负数
let arr = [1, 2, 3];
arr.length = -1;
console.log(arr);

RangeError: Invalid array length

// 对象方法参数超出范围
var num = new Number(66.66);
console.log(num.toFixed(-1));

RangeError: toFixed() digits argument must be between 0 and 100

类型错误

4、TypeError 类型错误

// 调用不存在的方法
123();

TypeError: 123 is not a function

var obj = {
    
    };
obj.say();

TypeError: obj.say is not a function

// 实例化原始值
var a = new 'string';

TypeError: "string" is not a constructor

URI 错误

5、URIError URI错误

URI: uniform resource identifier (统一资源标识符)

URL:uniform resource locator(统一资源定位符)

URN:uniform resource name(统一资源名称)

eval 函数执行错误

6、EvalError eval函数执行错误

这个就了解一下好了,在 es3就不建议使用了。它的性能不太好,也不好调试,同时可能会引起安全性问题xss攻击(例如 eval 和输入框 input值相绑定时容易引起 xss 安全性问题 ),另外,可读性也不太好(在字符串中执行)。

eval('var a=1; console.log(a);');

输出结果为 1

它神奇的地方在于,它可以将 json数据转换成对象。

try / catch

try {
    
    
  console.log('正常执行1');
  console.log(a);
  console.log('正常执行2');
} catch (error) {
    
    
  console.log(error.name + ':' + error.message);
}

输出结果如下:

正常执行1
ReferenceError:a is not defined

严格模式

ES5开始有了两种模式,一种是严格模式,一种是正常模式IE9及以下不支持严格模式

'use strict'; 为什么要设置字符串,是因为字符串相当于表达式,在浏览器不会报错。并且书写的位置必须在最上面一行。

补充知识点:with()语法就是括号里面填入哪个对象,就会找到对应作用域,也就是作用域链会被改变(严格模式下不能使用)

另外,arguments下大部分属性严格模式不能使用,例如之前提到的 callee / caller,但是 arguments可以使用。

其次,严格模式下这种代码也会报错,说 b没有声明:

'use strict';
var a = b = 1;

那我们看看下面会输出什么,是 1嘛?

function test(){
    
    
  console.log(this);
}
test.call(1);

结果如下,会打印包装类 Number,因为 this会指向对象,所以会进行一次包装。而在严格模式下就会输出 1

继续,再看看这份代码在 es5严格模式下会输出什么?

'use strict';
var obj = {
    
    
  a: 1,
  a: 2
}
console.log(obj.a);

答案是 2,这个比较特殊,严格模式下不报错。

补充:在 es5严格模式下,函数的参数名不能重复

继续,再看看这份代码在 es5严格模式下会输出什么?

'use strict';
eval('var a=1; console.log(a);');
console.log(a);

答案是 1 报错,因为严格模式下 eval有自己独立作用域,不会将 var a 放入全局使用了。

垃圾回收机制

垃圾回收机制就是负责管理代码执行过程中使用的内存。

原理:

第一步,找出不再使用的变量
第二步,释放其占用内存
第三部,固定的时间间隔运行

重新探究 this 指向问题

第一题

开门见山,我们来看看下面会打印什么?

function test(){
    
    
  this.a = 1;
  console.log(this);
  console.log(this.a);
}
test();
console.log(a);

答案是 window 1 1,不难发现,这个 this是指向 window的。

第二题

继续,看看下面代码又会输出什么?

var a = 1;
function test() {
    
    
  console.log(this);
  console.log(this.a);
}
test();

答案是 window 1,和上题类似。

第三题

继续,又会有怎样的输出呢?

function test(a){
    
    
  this.a = a;
  console.log(this.a);
  console.log(window.a);
}
test(1);

答案是 1 1

第四题

稍微改一下上面代码,又会输出什么呢?

function test(a){
    
    
  this.a = a;
  console.log(this.a);
  console.log(window.a);
}
new test(1);

答案是 1 undefined,因为 window底下并没有 a这个变量。

第五题

接下来,我们再来看看这道题,会打印什么呢?

function test(a){
    
    
  this.a = a;
  console.log(this.a);
}
var a = 223;
test.prototype.say = function(){
    
    
  console.log(this.a);
}
test.prototype.say();

答案是 undefiend,是不是很惊讶?为什么呢?我明明都访问了原型上的say方法,而且全局都有 a变量,为啥呢?

我们不妨打印一下 this,看指向谁。

function test(a){
    
    
  this.a = a;
  console.log(this.a);
}
var a = 223;
test.prototype.say = function(){
    
    
  console.log(this.a);
  console.log(this);
}
test.prototype.say();

然后发现在原型上打印 this,发现指向的是那个函数,而函数如果没有返回值的话,默认会返回 undefined,当然.a也会打印 undefined

第六题

于是我们修改一下代码:

function test(a){
    
    
  this.a = a;
  console.log(this.a);
}
var a = 223;
test.prototype.say = function(){
    
    
  console.log(a);
}
test.prototype.say();

答案是 223

第七题

因此在函数没有实例化的时候,原型上 say方法 this还是会指向那个函数。那么我们再次修改一下,看看下面代码会输出什么

function test(a){
    
    
  this.a = a;
}
var a = 223;
test.prototype.say = function(){
    
    
  console.log(this.a);
}
var t = new test(1);
t.say();

答案是 1,实例化了之后,原型上 say方法的 this 指向了实例对象。

defineProperty

function defineProperty(){
    
    
  var _obj = {
    
    };
  Object.defineProperty(_obj,'a',{
    
    
    value: 1
  });
  return _obj;
}
var obj = defineProperty();
console.log(obj);

结果如下,它可以对一个对象赋予对应属性。并且该对象原本就是空的

defineProperties

上述方式与我们直接通过 . 给对象赋值类似,那么怎么一下赋予多个属性呢,看如下代码:

function defineProperty(){
    
    
  var _obj = {
    
    };
  Object.defineProperties(_obj,{
    
    
    a: {
    
    
      value: 1
    },
    b: {
    
    
      value: 2
    }
  })
  return _obj;
}
var obj = defineProperty();
console.log(obj);


有了前置知识后,我们继续修改一下上述代码,看看又会有怎样的变化:

function defineProperty(){
    
    
  var _obj = {
    
    };
  Object.defineProperties(_obj,{
    
    
    a: {
    
    
      value: 1
    },
    b: {
    
    
      value: 2
    }
  })
  return _obj;
}
var obj = defineProperty();
obj.a = 5;
for(var k in obj){
    
    
  console.log(k + ':' + obj[k]);
}
delete obj.a;
console.log(obj);

结果如下:

我们发现得到的 obj 属性值不可改变,属性也不可以枚举,并且属性也不可以被删除。

因此,总结一下,通过 Object.defineProperty 配置的对象默认不可修改,不可枚举,不可删除

对于默认值,我们当然可以进行修改,查看下面代码注释处:

function defineProperty(){
    
    
  var _obj = {
    
    };
  Object.defineProperties(_obj,{
    
    
    a: {
    
    
      value: 1,
      writable: true, // 配置是否可修改
      enumerable: true, // 配置是否可以枚举
      configurable: true // 配置是否可以删除
    },
    b: {
    
    
      value: 2
    }
  })
  return _obj;
}
var obj = defineProperty();
obj.a = 5;
console.log(obj);
for(var k in obj){
    
    
  console.log(k + ':' + obj[k]);
}
delete obj.a;
console.log(obj);

结果如下:

数据劫持引入

看这标题,是不是突然觉得高大上了,但实则不是,那我们看一下下面代码会输出什么吧:

function defineProperty(){
    
    
  var _obj = {
    
    };
  var a = 1;
  Object.defineProperties(_obj,{
    
    
    a: {
    
    
      get(){
    
    
        return 'the value of a is ' + a;
      },
      set(newVal){
    
    
        console.log('the set value is' + newVal);
      }
    },
    b: {
    
    
      value: 2
    }
  })
  return _obj;
}
var obj = defineProperty();
console.log(obj.a);
obj.a = 1;

结果如下:

从上述结果我们可以总结归纳数据劫持的要点:

数据劫持,无非就是对待一个对象它的取值(get)和设置值(set)有一系列的配置和阻止的方法,这就是对一组数据属性的劫持。(即阻拦正常的输入输出)

对数组进行操作

我想你们应该都知道,defineProperty没办法对数组进行劫持。

function DataArr(){
    
    
  var _val = null,
      _arr = [];
  Object.defineProperty(this,'val',{
    
    
    get:function(){
    
    
      return _val;
    },
    set:function(newVal){
    
    
      _val = newVal;
      _arr.push({
    
    val: _val});
      console.log('A new value ' + _val + ' hash been pushed to _arr');
    }
  });
  this.getArr = function(){
    
    
    return _arr;
  }
}
var dataArr = new DataArr();
dataArr.val = 123;
dataArr.val = 234;
console.log(dataArr.getArr());

打印结果如下:

A new value 123 hash been pushed to _arr
A new value 234 hash been pushed to _arr
[ {
    
     val: 123 }, {
    
     val: 234 } ]

Proxy

let obj = new Proxy(target, handler);

defineProperty 它可以对一个对象赋予对应属性。并且该对象原本就是空的。而 proxy代理是操作原本就有属性的对象。

主要功能:自定义对象属性的获取、赋值、枚举、函数调用等

代理基础

那么,我们看看如下代码吧:

var target = {
    
    
  a: 1,
  b: 2
}
let proxy = new Proxy(target, {
    
    
  get(target, prop){
    
    
    console.log('This is property value ' + target[prop]);
  },
  set(){
    
    

  }
})
proxy.a;

答案是 This is property value 1。发现new了以后产生了一个新的代理对象,并且自定义了对象属性的获取。

继续,我们看看 set 方法又是怎样的呢?

var target = {
    
    
  a: 1,
  b: 2
}
let proxy = new Proxy(target, {
    
    
  get(target, prop) {
    
    
    console.log('This is property value ' + target[prop]);
  },
  set(target, prop, newVal) {
    
    
    target[prop] = newVal;
  }
})
proxy.a = 3;
proxy.a;
console.log(target);

打印结果为 This is property value 3{ a: 3, b: 2 },解释一下,尽管操作的代理对象,但是如果我们对代理对象进行了修改,原对象也会跟着改。

操作函数

let fn = function () {
    
    
  console.log('I am a function.');
}

fn.a = 123;

let newFn = new Proxy(fn, {
    
    
  get(fn, prop) {
    
    
    return fn[prop] + 'This is a Proxy return';
  }
})
console.log(newFn.a);

打印结果为 123This is a Proxy return

操作数组

let arr = [{
    
     name: 'Chocolate', age: 21 }, {
    
     name: 'jack', age: 20 }];

let persons = new Proxy(arr, {
    
    
  get(arr, prop) {
    
    
    return arr[prop];
  },
  set(arr, prop, newVal) {
    
    
    arr[prop] = newVal;
  }
})
console.log(persons[1]);
persons[2] = {
    
     name: 'leo', age: 18 };
console.log(persons, arr);

打印结果如下:

ES 14种操作对象的方法

获取原型

var obj = {
    
    a:1,b:2};
// 1.获取原型 [[GetPrototypeOf]]
var proto = Object.getPrototypeOf(obj);
console.log(proto);
console.log(obj.__proto__);
console.log(Object.prototype);

打印结果如下:

设置原型

var obj = {
    
     a: 1, b: 2 };
// 2.设置原型 [[SetPrototypeOf]]
Object.setPrototypeOf(obj, {
    
     c: 3, d: 4 });
// 同样也可以采用如下两种方式,直接赋值
// obj.__proro__ = xxx;
// Object.prototype = xxx;
console.log(obj);

打印结果如下:

获取对象的可拓展性

var obj = {
    
     a: 1, b: 2 };
// 3.获取对象的可拓展性 [[IsExtensible]]
var extensible = Object.isExtensible(obj);
console.log(extensible);
// 冻结对象
Object.freeze(obj);
var extensible2 = Object.isExtensible(obj);
console.log(extensible2);

打印结果为 true false

诶,从中我们发现了一个新东西 freeze,作用就是冻结对象,与之相关的还有一个 seal,叫做封闭对象,(简称自闭…开个玩笑^_^),还是举一下例子,对比一下,先来介绍 seal封闭对象:

var obj = {
    
     a: 1, b: 2 };
Object.seal(obj);
obj.c = 3;  // 不可修改
console.log(obj);
delete obj.a; // 不可删除
console.log(obj);
obj.b = 3; // 可写
console.log(obj);

打印结果如下,总结三点:不可修改、不可删除、可写。外加可读。

var obj = {
    
     a: 1, b: 2 };
Object.freeze(obj);
obj.c = 3;  // 不可修改
console.log(obj);
delete obj.a; // 不可删除
console.log(obj);
obj.b = 3; // 不可写
console.log(obj);
for (var key in obj) {
    
    
  console.log(obj[key]);
}

打印结果如下,总结三点:不可修改、不可删除、不可写,仅可读。

获取自有属性

var obj = {
    
     a: 1, b: 2 };
// 4.获取自有属性 [[getOwnProperty]]
Object.setPrototypeOf(obj, {
    
     c: 3, d: 4 });
console.log(Object.getOwnPropertyNames(obj));

答案是 [ 'a', 'b' ]

禁止拓展对象

var obj = {
    
     a: 1, b: 2 };
// 5.禁止拓展对象 [[PreventExtensions]]
Object.preventExtensions(obj);
obj.c = 3;
console.log(obj);
delete obj.a;
console.log(obj);

答案是 { a: 1, b: 2 }{ b: 2 },我们无法对 obj 对象进行拓展,但是可以进行删除操作。简单来说就是禁止增加属性,但可删除属性

拦截对象操作

// var obj = { a: 1, b: 2 };
// 6.拦截对象操作 [[DefineOwnProperty]]
// Object.defineProperty()

判断是否是自身属性

var obj = {
    
     a: 1, b: 2 };
// 7.判断是否是自身属性 [[HasProperty]]
console.log(obj.hasOwnProperty('a'));

答案是 true

获取对象属性

var obj = {
    
     a: 1, b: 2 };
// 8.获取对象属性 [[Get]]
console.log('c' in obj);
console.log('a' in obj);
console.log(obj.a);

打印结果如下:

false
true
1

设置对象属性

var obj = {
    
     a: 1, b: 2 };
// 9.设置对象属性 [[SET]]
obj.a = 3;
obj['b'] = 4;
console.log(obj);

答案{ a: 3, b: 4 }.

删除对象属性

var obj = {
    
     a: 1, b: 2 };
// 10.删除对象属性 [[Delete]]
delete obj.a;
console.log(obj);

答案是 { b: 2 }

枚举对象属性

var obj = {
    
     a: 1, b: 2 };
// 11. 枚举 [[Enumerate]]
for (var k in obj) {
    
    
  console.log(obj[k]);
}
1
2

获取键集合

var obj = {
    
     a: 1, b: 2 };
// 12.获取键集合 [[OwnPropertyKeys]]
console.log(Object.keys(obj));

答案 [ 'a', 'b' ]

调用函数

// 13.调用函数
var obj = {
    
     a: 1, b: 2 };
function test() {
    
     }
test();
obj.test = function () {
    
     }
obj.test();

new实例化对象

// 14.实例化对象
function Test() {
    
     };
new Test();

自己实现一个 Proxy

其实,有了前置知识,我们不难发现,ProxyObject.defineProperty 实现效果是一样的,但其实两个原理上是有挺大差别的。

  • defineProperty 操作的是空对象,而 proxy 操作的是现有对象。
  • defineProperty 原本目的是对一个对象赋予对应属性,而 proxy代理是操作原本就有属性的对象。其主要功能是自定义对象属性的获取、赋值、枚举、函数调用等

那么,我们首先看看 Proxy是怎样使用的,看一下下面这个例子,然后我们再用 defineProperty 自己实现一个 Proxy

let target = {
    
    
  a: 1,
  b: 2
}

let proxy = new Proxy(target, {
    
    
  get(target, prop) {
    
    
    return 'Get:' + prop + '=' + target[prop];
  },
  set(target, prop, newVal) {
    
    
    target[prop] = newVal;
    console.log('Set:' + prop + '=' + newVal);
  }
})

console.log(proxy.a);
proxy.b = 3;

打印结果如下:

Get:a=1
Set:b=3
let target = {
    
    
  a: 1,
  b: 2
}
function MyProxy(target, handler) {
    
    
  // 操作副本
  let _target = deepClone(target);
  Object.keys(_target).forEach((key) => {
    
    
    Object.defineProperty(_target, key, {
    
    
      get() {
    
    
        return handler.get && handler.get(target, key);
      },
      set(newVal) {
    
    
        return handler.set && handler.set(target, key, newVal);
      }
    })
  })
  return _target;
}
// 实现深拷贝
function deepClone(org, tar) {
    
    
  var tar = tar || {
    
    },
    toStr = Object.prototype.toString,
    arrType = 'Array';
  for (var key in org) {
    
    
    if (org.hasOwnProperty(key)) {
    
    
      if (typeof org[key] === 'object' && typeof org[key] !== 'null') {
    
    
        tar[key] = toStr.call(org[key]).slice(8, -1) === arrType ? [] : {
    
    };
        deepClone(org[key], tar[key]);
      } else {
    
    
        tar[key] = org[key];
      }
    }
  }
  return tar;
}

let proxy = new MyProxy(target, {
    
    
  get(target, prop) {
    
    
    return 'Get:' + prop + '=' + target[prop];
  },
  set(target, prop, newVal) {
    
    
    target[prop] = newVal;
    console.log('Set:' + prop + '=' + newVal);
  }
})
console.log(proxy.a);
proxy.b = 3;

打印结果如下,和上述源代码结果一样。

Get:a=1
Set:b=3

这里再进行梳理一下,MyProxy 实现原理是首先,先拷贝一份原对象,因为原本的 Proxy就是返回了一个代理对象,而我们先深拷贝一份对象,然后遍历这个拷贝对象,依次让对象的属性通过 Object.defineProperty 来实现数据劫持的效果,里面用到了 getset 方法,而getset时直接操作我们的原对象就是,这样当我们对代理对象(即我们一开始拷贝的对象)操作时,就会劫持我们的 getset方法,这样就能直接操作原对象了。

最终效果就是我们自定义了对象属性的获取、赋值的方式,不直接操作原对象,而是操作这个代理对象即可。

学习 Reflect

ReflectES6中出版的一个内置对象,也叫作反射。由于我们很多对象的方法都是放在 Object 上的,但是我们有时候并不是一直操作 Object,还有可能是函数或者数组等。这种情况下,我们放入 Object里面就不太合理。因此,ES6推出了Reflect,它是一个方法集合的容器。

而原本我们 get函数里面就是一个取值的方法,而并不是一个函数式的来操作对象,而 Reflect里面就有 get方法来做,同时也有对应的 set 方法,这样就是用底层方法来做了,而不是用一个等式来做,并且它是有返回值的(Boolean类型),可以确保我们 set成功。下面,我们来看一下下面这个经典例子吧:

let target = {
    
    
  a: 1,
  b: 2
}

let proxy = new Proxy(target, {
    
    
  get(target, prop) {
    
    
    return Reflect.get(target, prop);
  },
  set(target, prop, newVal) {
    
    
    const isOk = Reflect.set(target, prop, newVal);
    if(isOk){
    
    
      console.log('Set Successfully')
    }
  }
})
console.log(proxy.a);
proxy.b = 3;

打印结果如下:

1
Set Successfully

接下来,就是对于 Reflect的整理归纳:

  • Reflect可以看做是一个新的方法容器,未来许多方法都会逐步放入 Reflect中,而 Object主要用来构造原型、构造对象的,并且 Object里面方法是杂多的。
  • Object 返回值往往会抛出异常,例如像 definePropertyin 这样,通常会抛出异常。而 Reflect方法一般都有返回值 true / false,操作性更加合理。
  • Reflect里面操作对象的方法和 handler 几乎一致,除开枚举
  • Reflect属于 ES6中全局内置对象,在哪都可以访问,直接保存静态方法,例如 getsethas,不需要进行实例化操作。例如平常使用的 Math.random,也是可以直接使用的。

猜你喜欢

转载自blog.csdn.net/weixin_42429718/article/details/108678428
今日推荐