整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

熬夜7天,我总结了JavaScript与ES的25个

熬夜7天,我总结了JavaScript与ES的25个知识点


说起JavaScript,大家都知道是一门脚本语言。那么ES是什么鬼呢?ES全称ECMAScript ,是JavaScript语言的国际标准。

最近,我总结了25条JavaScript的基础特性相关的知识点,大家一起看一下吧

1.严格模式

  • 使用严格模式,可以在函数内部进行较为严格的全局和局部的错误条件检查
  • 严格模式的编译指示,"use strict"
  • 创建全局变量,未声明变量,非严格模式下为创建全局变量;严格模式下为抛出ReferenceError
  • 对变量调用delete操作符,删除变量,非严格模式下为静默失败;严格模式下为抛出ReferenceError
  • 操作对象情况下:a,只读属性赋值会抛出TypeError;b,对不可配置的属性使用delete操作符会抛出TypeError;c,为不可扩展的对象添加属性会抛出TypeError。
  • 重名属性情况:a,非严格模式下没有错误,以第二个属性为准;b,严格模式下会抛出语法错误。
  • 函数参数必须唯一,重名参数,在非严格模式下没有错误,只能访问第二个参数;严格模式下,会抛出错误。
function funValue(value) {    value="dada";    alert(value); // dada    alert(argument[0]); // 非严格模式:dada    // 严格模式模式 dadaqianduan}?funValue('dadaqianduan');
  • 访问arguments.callee和arguments.caller,在非严格模式下没有问题,严格模式下抛出TypeError。

2.Class基础语法


在JavaScript当中如何声明一个类?如何定义类中的方法?如何实例化对象?


我们来看看下面的代码示例:


// es5?let dada=function(type) {	this.type=type}?dada.prototype.study=function() {	console.log('魔王哪吒');}?let da1=new dada('程序员')let da2=new dada('It')?da1.constructor.prototype.study=function() {	console.log('dadaqianduan');}da1.study()

JavaScript constructor 属性

定义和用法

constructor 属性返回对创建此对象的数组函数的引用。

语法

object.constructor

constructor 是一种用于创建和初始化class创建的对象的特殊方法。

// es6class Da {  constructor(name) { // 构造函数内写属性    this.name=name;  }  eat() { // 构造函数外写方法  	console.log('i eat')  }}?const da1=new Da('da1');?console.log(da1.name); // da1console.log(da1);
  1. 一个类中只能有一个名为“constructor"的方法,出现多次构造函数constructor方法会抛出一个SyntaxError错误
  2. 在一个构造方法中可以使用super来调用一个父类的构造方法
  3. 如果没有指定一个构造函数方法constructor方法,就会使用一个默认的构造函数

3.类的属性Setter和Getter

var daObj={ get val() {  return ; }, set val(value) { }}

get:

var da={    a: 1,    get val(){        return this.a + 1;    }}?console.log(da.val);//2da.val=100;console.log(da.val);//2?class Da { constructor(type) {  this.type=type } get age() {  return 1 } set age(val) {  this.realAge=val } eat() {  console.log('i am eat') }}let da1=new Da('da1')console.log(da1.age)da1.age=1console.log(da1.realAge)
class Da { constructor(type, age) {  this.type=type  this.age1=age } get age() {  return this._age } set age(val) {  this._age=val }}

利用set/get实现对element.innerHTML封装

class myHTMLElement { constructor(element) {  this.element=element } get html() {  return this.element.innerHTML } set html(value) {  this.element.innerHTML=value }}

设置一个闭包,通过一定的规则来限制对它的修改:

let myName='dada'class Da { constructor(type) {  this.type=type } get name() {  return myName } set name(val) {  myName=val }}

4.静态方法

在es5中实现的静态方法:

let Da=function (type) { this.type=type this.eat=function() {  console.log('i eat') }}Da.study=function(book) { console.log('i book');}
let Da=function(type) { this.type=type}Da.prototype.eat=function() { Da.walk() console.log('i am')}Da.walk=function(){ console.log('walk')}let da1=new Da('da1')da1.eat()?// walk// i am

静态方法在你的实例化对象是找不到的

在es6中的静态方法,标记static

class Da { constructor(type) {  this.type=type } eat() {  console.log('i eat') } static study() {  console.log('i study') }}

5.如何继承一个类

在es5中的继承:

// 定义一个父类let Da=function(type) { this.type=type}// 定义方法Da.prototype.eat=function() { console.log('i am')}// 定义静态方法Da.study=function(book) { console.log('i study')}// 定义子类let Da1=function() { // 初始化父类 Da.call(this, 'da1'); this.run=function() {  console.log('i run') }}// 继承Da1.prototype=Da.prototype

在es6中的继承

class Da { constructor(type) {  this.type=type } eat() {  // Da.walk();  console.log('i eat') } static walk(){  console.log('i walk') }}?class da extends Da { // 构造函数 //constructor (type) {  //super(type) //} run() {  console.log('i run') }}let da1=new da('da1')

6.面向对象编程Class

类的声明,属性,方法,静态方法,继承,多态,私有属性

// 类的声明let Da=function(type) { this.type=type this.eat=function() {  console.log('i eat'); }}?let da=new Da('da');
// prototypelet Da=function(type) { this.type=type}Da.prototype.eat=function() { console.log('i eat')}let da1=new Da('da1')

es6中的Class

class Da { // 构造函数 constructor(type) {  this.type=type } // 方法 walk() {  console.log('i walk') }}let da=new Da('da');// console.log(typeof Da); function

7.函数参数的默认值

函数参数是从左到右解析,如果没有默认值会被解析成 undefined

// 参数默认值function da (x,y,z) {}function sum() { let num=0 Array.prototype.forEach.call(arguments, function(item){  num +=item * 1 }) Array.from(arguments).forEach(function(item){  num +=item * 1 }) return num}
// 不确定function sum(...nums) { let num=0 nums.forEach(function(item){  num +=item * 1 }) return num}console.log(sum(1,2,3,4,5))
function sum () {  let num=0  Array.prototype.forEach.call(arguments, function (item) {    num +=item * 1  })  return num}?function sum (...nums) {  let num=0  nums.forEach(function (item) {    num +=item * 1  })  return num}

8.es6箭头函数

箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。

()=> {}// function Da() {}// let da=function() {}let da=()=> { console.log('hello')}da()?let da=name=> {}
const materials=[  'Hydrogen',  'Helium',  'Lithium',  'Beryllium'];console.log(materials.map(material=> material.length));// expected output: Array [8, 6, 7, 9]

拓展

判断函数有几个参数

  1. 在 ES5 中可以在函数体内使用 arguments 来判断。
  2. 在 ES6 中可以借助 Function.length 来判断。(统计第一个默认参数前面的变量数)
console.log(sum(...[4]))console.log(sum.apply(null, [4]))

9.JavaScript中的三个点(…)

JavaScript当中,函数的参数前面有三个点,代表什么呢?我们看下代码示例:

function myFunc(a, b, ...args) { console.log(a); // 22 console.log(b); // 98 console.log(args); // [43, 3, 26]};myFunc(22, 98, 43, 3, 26);
function myFunc(x, y, ...params) { // used rest operator here  console.log(x);  console.log(y);  console.log(params);}?var inputs=["a", "b", "c", "d", "e", "f"];myFunc(...inputs); // used spread operator here// "a"// "b"// ["c", "d", "e", "f"]
var obj1={ foo: 'bar', x: 42 };var obj2={ foo: 'baz', y: 13 };?var clonedObj={ ...obj1 };// Object { foo: "bar", x: 42 }?var mergedObj={ ...obj1, ...obj2 };// Object { foo: "baz", x: 42, y: 13 }

10.Object Property

JS中对象的属性定义,代码示例如下:

let x='da1';let y='da2';let obj={ x, y}console.log(obj);?// 结果{x:'da1',y:'da2'}
let x=1; let y=2; let z=3let obj={ 'x': x, y, [z+y]: 4, * hello() { // 异步  console.log('dada') }}// function* functionName() {}obj.hello()

其中,*hello是Generator函数,这是ES6提供的一种异步解决方案。

11.Set数据结构

Set存储的成员不允许的重复的(它类似于数组)

Set 本身是一个构造函数,用来生成 Set 数据结构。

const s=new Set();?[2, 3, 5].forEach(x=> s.add(x));?Set 函数可以接受一个数组(或类似数组的对象)作为参数,用来初始化?const set=new Set([1, 2, 3, 4, 4]);

实现数组去重

var arr=[1,1,2,2,3,3]; // step1:数组转集合var s=new Set(arr); // 已经去掉重复值,当前不是数组,而集合s.size; // 3// step2:集合转数组console.log([...s]); // 1,2,3;?// Array.form 方法可以将 Set 结构转为数组const items=new Set([1, 2, 3]);const arr=Array.from(items);?function dada(array) {  return Array.from(new Set(array));}?dada([1, 1, 2])

Set的遍历

  • keys():返回键名的遍历器
  • values():返回键值的遍历器
  • entries():返回键值对的遍历器
  • forEach():使用回调函数遍历每个成员

操作方法

  • add(value):添加某个值,返回Set结构本身。
  • delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • has(value):返回一个布尔值,表示该值是否为Set的成员。
  • clear():清除所有成员,没有返回值。
let set=new Set([1, 2, 3, 4, 4]);?// 添加数据 let addSet=set.add(5);console.log(addSet); // Set(5) {1, 2, 3, 4, 5}?// 删除数据 let delSet=set.delete(4);console.log(delSet); // true?// 查看是否存在数据 4let hasSet=set.has(4);console.log(hasSet); // false?// 清除所有数据set.clear();console.log(set); // Set(0) {}

实现并集(Union)、交集(Intersect)和差集(Difference)

let a=new Set([1, 2, 3]);let b=new Set([4, 3, 2, 1]);?// 并集let union=new Set([...a, ...b]);// Set {1, 2, 3, 4}?// 交集let intersect=new Set([...a].filter(x=> b.has(x)));// set {1, 2, 3}?// 差集let difference=new Set([...b].filter(x=> !a.has(x)));// Set {4}

12.Map数据结构

JS当中的哈希表,使用方法如下:

let map=new Map()map.set(1, 2)map.set(3, 4)map.set(1, 3)console.log(map)?创建var da=new Map();var jeskson={};遍历da.forEach(function(value,key,map){}长度da.size删除//da.delete() 删除key,全部清楚da.clear()新增da.set(key,value)da.has(查索引值)?da.forEach((value,key)=>{})?for( let [key, value] of map){}?// let map=new Map( [[1,2], [3,4]] )?map的key任意都可以let o=function() { console.log('o')}map.set(o, 3)console.log(map.get(o)); // 3
// map.jsvar Dictionary=function() { var items={}; // 检查键 this.has=function(key) {  return key in items; } // 添加键值对 this.set=function(key, value){  items[key]=value; } // 通过键移除元素 this.delete=function(key) {  if(this.has(key)){   delete items[key]   return true  }  return false } // 键获取值 this.get=function(key){  return this.has(key) ? items[key] : undefined; } // 列表返回字典值 this.values=function() {  var values=[];  for(var k in items) {   if(this.has(k)) {    values.push(items[k])   }  }  return values; } // 获取全部键名 this.keys=function() {  return Object.keys(items); } // 获取私有变量items this.getItems=function() {  return items; }}

Map数据结构,它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

13.Object.assign(对象的拷贝)

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

const target={ a: 1, b: 2 };const source={ b: 4, c: 5 };?const returnedTarget=Object.assign(target, source);?console.log(target);// expected output: Object { a: 1, b: 4, c: 5 }?console.log(returnedTarget);// expected output: Object { a: 1, b: 4, c: 5 }?> Object { a: 1, b: 4, c: 5 }> Object { a: 1, b: 4, c: 5 }

语法

Object.assign(target, ...sources)

参数

target

目标对象

sources

源对象

返回值

目标对象。

const obj={ a: 1 };const copy=Object.assign({}, obj);console.log(copy); // { a: 1 }
  • Object.assign()拷贝的是(可枚举)属性值
  • Object.assign方法的第一个参数是目标对象,后面的参数都是源对象
  • 如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性
  • 由于undefined和null无法转成对象,所以如果它们作为参数,就会报错
  • 如果undefined和null不在首参数,就不会报错
  • 如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用(这个对象的任何变化,都会反映到目标对象上面。)
Object.assign(undefined) // 报错Object.assign(null) // 报错?let obj={a: 1};Object.assign(obj, undefined)===obj // trueObject.assign(obj, null)===obj // true?const obj1={a: {b: 1}};const obj2=Object.assign({}, obj1);?obj1.a.b=2;obj2.a.b // 2?const target={ a: { b: 'c', d: 'e' } }const source={ a: { b: 'hello' } }Object.assign(target, source)// { a: { b: 'hello' } }?const source={  get foo() { return 1 }};const target={};?Object.assign(target, source)// { foo: 1 }

Object.assign复制的是属性值value,如果属性值是一个引用类型,那么复制的其实是引用地址,就会存在引用共享的问题(Object.assign(target,source1,...,sourceN)浅拷贝的过程)

要点:

function ObjectAssign(target, ...sources) { // 对第一个参数的判断,不能为undefined和null if(target===undefined || target===null) {  throw my TypeError('error'); } // 将第一个参数转为对象(不是对象转换为对象) const targetObj=Object(target); // 将源对象自身的所有可枚举属性复制到目标对象 for(let i=0; i<sources.length; i++){  let source=sources[i];  // 对于undefined和null在源中不会报错,会直接跳过  if(source !==undefined && source !==null) {   // 将源象转换成对象   // 需要将源自身的可枚举数据进行复制   // Reflect.ownKeys(obj)   const keysArray=Reflect.ownKeys(Object(source));   for (let nextIndex=0; nextIndex < keysArray.length; nextIndex++) {    const nextKey=keysArray[nextIndex];    // 去除不可枚举属性    const desc=Object.getOwnPropertyDescriptor(source,nextKey);    if(desc!==undefined&&desc.enumerable){     targetObj[nextKey]=source[nextKey];    }   }  } } return targetObj;}if(typeof Object.myAssign !=='function'){ Object.defineProperty(Object, 'myAssign', {  value: ObjectAssign,  writable: true,  enumerable: false,  configurable: true });}

浅拷贝 Object.assign 的实现原理

拷贝第一层的基本类似值和第一层的引用类型地址:

let da1={ name: 'da1', age: 1}?let da2={ name: 'da2', study: {  title: 'web' }}?let da3=Object.assign(da1,da2);console.log(da3);// {// name: 'da2',// age: 1,// study: { title: 'web' }// }console.log( da1===da3); // true?da2.name='da22';da2.study.title='web2';console.log(da2);// {// name: 'da22',// study: { title: 'web2' }// }?console.log(da1);// {// age: 1,// name: 'da2',// study: { title: 'web2' }// }

如果源对象的属性值是一个指向对象的引用,它也只拷贝这个引用地址哦!

let da1={ name: 'da1', age: 1}let da2={ a: Symbol('dadaqianduan'), b: null, c: undefined}let da3=Object.assign(da1, da2);console.log(da3);// {// name: 'da1',// age: 1,// a: Symbol('dadaqianduan'),// b: null,// c: undefined// }console.log(da1===da3); // true
let map=new Map([iterable])// Map是用来实现字典的功能-Object键值对

动态属性键

// ES5 codevar  key1='one',  obj={    two: 2,    three: 3  };?obj[key1]=1;?// obj.one=1, obj.two=2, obj.three=3?// ES6 codeconst  key1='one',  obj={    [key1]: 1,    two: 2,    three: 3  };?// obj.one=1, obj.two=2, obj.three=3?// ES6 codeconst  i=1,  obj={    ['i' + i]: i  };?console.log(obj.i1); // 1

补充:前端面试考点,HTML和CSS,性能优化,原型,作用域,异步,各种手写代码,DOM事件和Ajax,HTTP协议。

  • css(布局,定位,移动端响应式)
  • es(原型,原型链,作用域,闭包,异步,单线程)
  • webapi(DOM BOM,Ajax跨域,事件存储)
  • 开发环境(版本管理,调试抓包,打包构建)
  • 运行环境(页面渲染,性能优化,web安全)
  • 网络通讯
  1. 布局(盒模型,BFC,float,flex)
  2. 定位,图文样式,移动端响应式(rem,media query,vw/vh),动画、渐变
  3. 变量类型和计算(值类型和引用类型,类型判断,逻辑运算)
  4. 原型和原型链(class,继承,原型,原型链,instanceof)
  5. 作用域和闭包(作用域,自由变量,闭包,this)
  6. 异步(单线程,callback,应用场景,Promise,event-loop,async/await,微任务/宏任务)
  7. 模块化(ES6 Module)
  8. DOM(树形结构,节点操作,属性,树结构操作,性能)
  9. BOM(navigator,screen,location,history)
  10. 事件(绑定,冒泡,代理)
  11. ajax(XMLHttpRequest,状态码,跨域)
  12. 存储(cookie,localStorage,sessionStorage)
  13. 开发环境(git,调试,webpack和babel,linux命令)
  14. 运行环境(页面加载:加载,渲染。性能优化:加载资源优化,渲染优化。安全:xss,CSRF)
  15. HTTP协议:状态码,Method,Restful API,headers,缓存策略

14.模板文字

模板文字是es2015/es6的新功能,与es5及以下版本相比,可以通过新颖的方式使用字符串,先只需要反引号代替单引号或双引号即可:

const module_string=`dadaqianduan`

它们之所以独特是因为它们提供了很多用引号构建的普通字符串不具备的功能:

  1. 提供了定义多行字符串的语法;
  2. 提供了一种简单的方法来插值字符串中的变量和表达式
  3. 允许您使用模板标签创建DSL(领域特定的语言)

使用多行字符串

在es6之前的版本:

// 要创建跨越两行的字符串,必须\在行尾使用字符?const dada='dada \  dadaqianduan'  // 呈现效果:在两行上创建一个字符串,但是仅在一行上呈现

要在多行上呈现,则需要使用\n在每行的末尾添加

const string='dada 魔王哪吒\n \  dadaqianduan'

使用反引号打开模板文字后,只需要按enter键就行:

const dada=`dadaqianduan 魔王哪吒`

在这里请记住空间是有意义的:

const da=`First            Second`

使用trim()方法,可以消除第一个字符之前的任何空格

插补:模板文字提供了一种将变量和表达式插入字符串的简便的方法

const da=`dadaqianduan ${mydada}`?${}里面可以添加任何东西?const da1=`dada ${1+2+3}`const da2=`dada ${dafun() ? 'x' : 'y'}`

15.什么是解构赋值

let da=['hello', 'world']let [firstName, surName]=dacosole.log(firstName, surName);

解构赋值在于赋值,拷贝出来赋值给变量,而赋值的元素本身不会发生改变

默认值

let [da1, da2]=[];?console.log(da1); // undefinedconsole.log(da2); // undefined

给变量赋值(默认值),防止出现undefined的情况:

let [da1='da1', da2='da2']=['dadaqianduan]?console.log(da1); // dadaqianduanconsole..log(da2); // da2

解构分配

ES5中的索引提取这些值:

var myArray=['a', 'b', 'c'];var  one=myArray[0],  two=myArray[1],  three=myArray[2];?// one='a', two='b', three='c'

ES6解构允许使用更简单方法:

const [one, , three]=myArray;?// one='a', three='c'

使用rest运算符(...)提取剩余元素:

const [one, ...two]=myArray;?// one='a', two=['b, 'c']
const myObject={  one:   'a',  two:   'b',  three: 'c'};?// ES6 destructuring exampleconst {one: first, two: second, three: third}=myObject;?// first='a', second='b', third='c'

可变值交换

var a=1, b=2;?// ES5 swapvar temp=a;a=b;b=temp;?// a=2, b=1?// ES6 swap back[a, b]=[b, a];?// a=1, b=2?[b, c, d, e, a]=[a, b, c, d, e];

在ES6中,我们可以为任何参数分配默认值

function dada(param={}) {

函数返回多个值(函数只能返回一个值,但可以是一个复杂的对象或多维数组)

function f() {  return [1, 2, 3];}?const [a, b, c]=f();?// a=1, b=2, c=3

ES6 JavaScript深度解构

默认情况下,找不到的属性为undefined

var {da}={bar: 'dada'}console.log(da)// undefined

如果访问不存在的父级的深层嵌套属性,则将获得异常。

var {da:{bar}}={dada: 'dadaqianduan'}// Exception
var key='dadaqianduan'var { [key]: foo }={ dadaqianduan: 'bar' }console.log(foo)// 'bar'
var {da=3}={ da: 2 }console.log(da)// 2var {da=3}={ da: undefined }console.log(da)// 3var {da=3}={ bar: 2 }console.log(da)// 3?var [a]=[]console.log(a)//  undefinedvar [b=10]=[undefined]console.log(b)//  10var [c=10]=[]console.log(c)//  10?function da () {  return {    x: 1,    y: 2  }}var {x, y}=da()console.log(x)// 1console.log(y)// 2

16.异步操作

Callback

Promise

function loadScript(src) { return new Promise((resolve, reject)=> {  let script=document.createElement('script')  script.src=src  script.onload=()=> resolve(src)  script.onerror=(err)=> reject(err)  document.head.append(script) })}
function loadScript(src) { let script=document.createElement('script'); script.src=src; document.head.append(script)}
var promise=new Promise(function(resolve, reject){ resolve('传递给then的值')})promise.then(function(value){ console.log(value)},function(error){ console.error(error)})

Promise对象是用于表示一个异步操作的最终完成(或失败),以及其结果值。

示例:

const promise=new Promise((resolve, reject)=> { setTimeout(()=> {  resolve('da'); }, 200);});?promise.then((value)=> { console.log(value);});?console.log(promise);

语法:

new Promise(function (resolve,reject){...});

描述:Promise对象是一个代理对象,被代理的值在Promise对象创建时可能是未知的,它允许你为异步操作的成功和失败分别绑定相应的处理方法,这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象。

一个Promise有以下几种状态:

  1. pending,初始状态,既不是成功,也不是失败状态。
  2. fulfilled,意味着操作成功完成。
  3. rejected,意味着操作失败。

pending状态的Promise对象可能会变为fulfilled状态并传递一个值给相应的状态处理方法。

Promise.prototype.then和Promise.prototype.catch方法返回promise对象,所以它们可以被链式调用。

方法:

Promise.all(iterable)

  1. 返回一个新的promise对象
  2. 在iterable参数对象里所有的promise对象都成功时,才会触发成功
  3. 当任何一个iterable里面的promise对象失败,才会触发promise对象的失败
  4. 成功状态会把一个包含iterable里所有promise返回值的数组作为成功回调的返回值,顺序和iterable的顺序一样
  5. 如果这个新的promise对象触发了失败,会把iterable里的第一个触发失败的promise对象的错误信息作为它的失败信息
  6. 场景,多用于处理多个promise对象的状态集合

Promise.any(iterable)

  1. 接收一个Promise对象的集合,当其中的一个promise成功,就返回那个成功的promise的值

Promise.reject(reason)

  1. 返回一个状态为失败的Promise对象,然后将失败信息传递给对应的处理方法

Promise.resolve(value)

  1. 返回一个状态由给定value决定的Promise对象

Promise原型

属性:Promise.prototype.constructor返回被创建的实例函数,默认为Promise函数。

方法:

  • Promise.prototype.catch(onRejected)
  • Promise.prototype.then(onFulfilled,onRejected)
  • Promise.prototype.finally(onFinally)
function myAsyncFunction(url) { return new Promise((resolve, reject)=> {  const xhr=new XMLHttpRequest();  xhr.open('GET',url);  xhr.onload=()=> resolve(xhr.responseText);  xhr.onerror=()=> reject(xhr.statusText);  xhr.send(); });}

17.ES6代理

  1. 默认情况下,代理不执行任何操作

示例:

var target={}var handler={}var proxy=new Proxy(target, handler)proxy.a='b'console.log(target.a)// 'b'console.log(proxy.c===undefined)// true

为了更好地了解代理的有用性,让我们做一些小练习。

示例:

想象一下,您已经17岁了,即将满18岁。并且您希望您的程序在打开时自动向您祝贺。为此,您可以使用代理。

var person={  name: "dada",  age: 17};?person=new Proxy(person, {  set(target, property, value) {    if (value===18) {      console.log("Congratulations! You are of legal age");      Reflect.set(target, property, value);      return true;    }  }});?person.age=18;?if (value < 13 && value > 99) {  throw new Error('The age should be between 13 and 99')} else {  Reflect.set(target, property, value)}

语法:

let p=new Proxy(target, handler)
  1. target 用Proxy包装的目标对象
  2. handler 一个对象,其属性是当执行一个操作时定义代理的行为的函数

如果不想再调用key的时候,返回undefined:

console.log(o.dada || '')

使用Proxy

let o={ name:'dada', age: 1}?let handler={ get(obj, key) {  return Reflect.has(obj, key)?obj[key]:'' }}?let p=new Proxy(o, handler)?console.log(p.from)

希望从服务器获取的数据只读,不允许修改:

for (let [key] of Object.entries(response.data)) {  Object.defineProperty(response.data, key, {  writable: false })}

使用Proxy:

let data=new Proxy(response.data, { set(obj, key, value) {   return false }})

检验逻辑代码:

// Validator.js?export default(obj, key, vlaue)=> { if(Reflect.has(key) && value > 20) {  obj[key]=value }}?import Validator from './Validator'?let data=new Proxy(response.data, { set: Validator})

使用Proxy,对读写进行监控:

let validator={ set(target, key, value) {  if(key==='age') {   if(typeof value !=='number' || Number.isNaN(value)) {    throw new TypeError('Age must be a number')   }   if(value<=0){    throw new TypeError('Age must be a positive number')   }  }  return true }}?const person={ age: 12 }const proxy=new Proxy(person,validator)proxy.age='dada' // TypeError numberproxy.age=NaNproxy.age=0 // positive numberproxy.age=3

示例:每个对象都有一个自己的id

class Component { constructor() {  this.proxy=new Proxy({   id: Math.random().toString(36).slice(-8)  }) } get id() {  return this.proxy.id }}

18.Generator

function * dada() { for(let i=0; i<2; i++ {  yield console.log(i); }}?const da=dada()da.next()da.next()

Generator函数与普通函数的区别在于定义的时候有一个*,执行下面函数:

function* dada() {console.log('dadaqianduan');}dada(); // 没有执行函数 如需要输出,改为:var da=dada();da.next();

要生成一个自增的id:

var count_id=0;function dada_id() {count_id ++;return count_id;}

方法

Generator.prototype.next()返回一个由 yield表达式生成的值。?Generator.prototype.return()返回给定的值并结束生成器。?Generator.prototype.throw()向生成器抛出一个错误。

书写风格:

function *da() {}?function* da(){}

方法

Generator对象方法:next,return,throw

通过Next方法来获取每一次遍历的结果,这个方法返回一个对象,这个对象包含两个value和done。

value:当前程序的运行结果 done:遍历是否结束

next是可以接收参数的,这个参数可以让你在generator外部给内部传递数据,这个参数就是作为yield的返回值。

return()方法可以让generator遍历终止

function * da() { yield 1 yield 2 yield 3}var d=da()console.log(d.next()) // {value:1,done:false}console.log(d.return()) // {value:undefined,done:true}console.log(d.next()) // {value:undefined,done:true}

return可以传入参数,作为返回的value的值

function * da() { yield 1 yield 2 yield 3}var d=da()console.log(d.nex()) // {value:1,done:false}console.log(d.return(100)) // {value:100,done:true}console.log(d.next()) // {value:undefined,done:true}

throw()方法在generator外部控制内部执行的“终断”

generator函数声明:

function* genFunc(){...}const genObj=genFunc();

generator表达式:

const genFunc=function* () {...}const genObj=genFunc();

对象中定义:

const obj={ * generatorMethod(){  ... }}const genObj=obj.generatorMethod();

类定义(类声明或类表达式):

class MyClass{ * generatorMethod(){  ... }}const myInst=new MyClass();const genObj=myInst.generatorMethod();

最简单的iterator遍历规范:

authors[Symbol.iterator]=function(){ // this return {  next(){   return{    done:false,    value:1   }  } }}

19.module

在es6前,js文件之间的导入,导出是借助require.js,sea.js,如现在使用import,export,来实现原生javascript的导入,导出。

export:

导出变量或者常量?export const da='dadaqianduan'export let da1='da1'export var da2='da1'?const name='dada'let name1='dada1'export { name, name1}?导出函数export function da(value){ console.log(value)}?const da=(value)=> { console.log(value);}?export { da}?导出Objectexport({ name: 'da1', message: 'dadaqianduan'})?let da={ name: 'name1'}export { da}?导出Classclass Da { constructor(){  this.id=1 }}export { Da}?export class Da { constructor() {  this.id=1 }}?修改导出名称const name='da1'export { name as cname}export default name

import

// 直接导入const name='dada'let name1='dada1'var name2='dada2'export { name as cname}export default name2?import name2, {name1, name} from A
export const sqrt=Math.sqrt;export function square(x) { return x * x;}export function dada(x,y) { return sqrt(square(x) + square(y));}?import {square,da} from 'da';console.log(square(11)); // 121console.log();
export default function() {...}import myFunc from 'myFunc';export default class{...}import MyClass from 'MyClass';const inst=new MyClass();

20.import, export

require--lib.js--function add(x,y){ return x + y}module.exports={ add: add,};?--main.js--var add=require('lib').addd;console.log(add(1,2));
import--lib.js--export function add(x,y) { return x + y}--main.js--import {add} from 'lib';console.log(add(1,2));
--lib.js--export const sqrt=Math.sqrt;export function square(x) { return x * x;}export function da(x,y) { return sqrt(square(x)+square(y));}--main.js--import {square, da} from 'lib'??--myFunc.js--export default function() {...};--main.js--import myFunc from 'myFunc';myFunc();

21.Array.prototype.includes,Promise

该方法判断一个数组是否包含一个指定的值,返回布尔值

let da1=[1,2,3];console.log(da1.includes(2));
arr.find(function(item){return item===1;})?arr.filter(function(item){return item===2;})?Math.pow(2,3)->2**3
async function firstAsync(){ let promise=new Promise ((resolve,reject)=> {  setTimeout(function(){   resolve('dadaqianduan')  },1000) }) console.log(await promise) console.log(await Promise.resolve(1)) console.log(2) return Promise.resolve(3)}firstAsync().then(val=> { console.log(val)})

await后面是Promise对象

Object.values()返回一个数组,其元素是在对象上找到的可枚举属性值。

let da={ 'da': 1, 'da2': 2}console.log(Object.value(da)) // [1,2]?Object.values是在对象上找到可枚举的属性的值,所以只要这个对象是可枚举的就可以

Object.entries()方法返回一个给定对象自身可枚举属性的键值对数组

22.JS异步进阶

题目一:

Promise.resolve().then(()=>{ console.log(1)}).catch(()=>{ console.log(2)}).then(()=>{ console.log(3)})

题目二:

Promise.resolve().then(()=>{ console.log(1) throw new Error('da')}).catch(()=>{ console.log(2)}).then(()=>{ console.log(3)})

题目三:

Promise.resolve().then(()=>{ console.log(1) throw new Error('da')}).catch(()=>{ console.log(2)}).catch(()=>{ console.log(3)})

题目四:

async function fn() { return 1}(async function() { const a=fn() // ?? const b=await fn() // ??})()

题目五:

console.log(100)setTimeout( ()=> { console.log(200)})Promise.resolve().then( ()=> { console.log(300)})console.log(400)

题目六:

async function async1() { console.log('async1 start') await async2() console.log('async1 end')}?async function async2 () { console.log('async2')}?console.log('script start')?setTimeout(function(){ console.log('setTimeout')},0)?async1()?new Promise(function (resolve){ console.log('promise1') resolve()}).then(function(){ console.log('promise2')})?console.log('script end')

加载图片:

// 加载function  loadImg(src) { const p=new Promise(  (resolve,reject)=> {   const img=document.createElement('img')   img.onload=()=>{    resolve(img)   }   img.onerror=()=>{    const err=new Error('图片加载失败')    reject(err)   }   img.src=src  } ) return p}const url='https'const p=loadImg(url)?p.then(img=>{ console.log(img.width) return img}).then(img=>{ console.log(img.height)}).catch(ex=> { console.error(ex)})
async function async1() { console.log('async1 start') // 2 await async2() // undefined console.log('async1 end') // 5}async function async2() { console.log('async2') // 3}console.log('script start') // 1async1()console.log('script end') // 4

for...of常用于异步的遍历

function add(num) { return new Promise(resolve=> {  setTimeout(()=>{   resolve(num*num)  },1000) })}const nums=[1,2,3]nums.forEach(async(i)=>{ const res=await add(i)})

23.宏任务和微任务

宏任务:setTimeout,setInterval,ajax等 微任务:Promise async/await

微任务执行时比宏任务要早:

宏任务:DOM渲染后触发,如setTimeout

微任务:DOM渲染前触发,如Promise

24.For await of 异步操作集合

function da(time) { return new Promise(function(resolve,reject){  setTimeout(function(){   resolve(time)  },time) })}async function test() { let arr=[da(2000),da(1000),da(3000)] for await (let item of arr) {  console.log(Date.now(), item) }}
const input={ a: 1, b: 2}const output={ ...input, c: 3}console.log(output)?const input={ a: 1, b: 2, c: 3}let {a, ...rest }=input

25.Array.prototype.flat()

该方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合为一个新数组。

Array.prototype.flat()建议将数组递归展平至指定范围depth并返回新数组。

depth(指定要提取嵌套数组的结构深度)

语法:Array.prototype.flat(depth)

depth —默认值1,Infinity用于展平所有嵌套数组。

const numbers=[1, 2, [3, 4, [5, 6]]];?// Considers default depth of 1numbers.flat(); ?> [1, 2, 3, 4, [5, 6]]?// With depth of 2numbers.flat(2); ?> [1, 2, 3, 4, 5, 6]?// Executes two flat operationsnumbers.flat().flat(); ?> [1, 2, 3, 4, 5, 6]?// Flattens recursively until the array contains no nested arrays?numbers.flat(Infinity)> [1, 2, 3, 4, 5, 6]

语法:Array.prototype.flatMap(callback)

callback:function产生新Array的元素。

const numbers=[1, 2, 3];?numbers.map(x=> [x * 2]);> [[2], [4], [6]]?numbers.flatMap(x=> [x * 2]);> [2, 4, 6]

Object.fromEntries

Object.fromEntries执行与的相反操作Object.entries。它将一组键值对转换为一个对象。

const records=[['name','da'], ['age', 32]];?const obj=Object.fromEntries(records);?> { name: 'da', age: 32}?Object.entries(obj);?> [['name','Mathew'], ['age', 32]];

Symbol.prototype.description

只读属性,返回Symbol对象的可选描述:

Symbol('desc').toString();> "Symbol(desc)"?Symbol('desc').description;  > "desc"?Symbol('').description;      > ""?Symbol().description;> undefined

点关注,不迷路

好了各位,以上就是这篇文章的全部内容,能看到这里的人都是人才。我后面会不断更新技术相关的文章,如果觉得文章对你有用,欢迎给个“赞”,也欢迎分享,感谢大家 !!


喜欢本文的朋友,欢迎关注公众号 程序员小灰,收看更多精彩内容

日,在世纪之初风靡全球、一时风头无两的女星“小甜甜”布兰妮·斯皮尔斯再次出现在公众视野。但这一次,不是因为她让万人空巷的全球巡演,不是因为她热销3000万张的个人专辑,而是因为她请求法院解除父亲杰米·斯皮尔斯对自己的永久监护权。

“小甜甜”布兰妮·斯皮尔斯。

在6月23日的庭审中,布兰妮通过语音连线,向法官讲述了过去13年中,自己遭受父亲及其工作团队的虐待,具体内容包括被迫长时间、高强度工作,接受非自愿并有较大副作用的精神治疗,安置节育环以致她无法再次怀孕。此外,处于监护状态下的她,完全丧失了对自己财产的使用权利,连简单的度假、美甲、按摩等需求都无法得到父亲批准,“在加州,只有被关起来的性工作者才这么惨——信用卡、现金、手机、护照全部被收走。”

在通话的最后,布兰妮说道:“我希望我能一直和你聊下去。我生怕一挂掉电话,就会立刻回到被人全盘否定的状态。我觉得自己被排挤、被欺负、被冷落……我受够了这种孤独的状态。我值得拥有和任何人一样的权利,我也想有孩子、有家庭、有自己的人生……”

2008年,布兰妮因使用精神类药物而被强制送往精神医疗中心治疗。之后,她的父亲杰米向法院申请,成为了布兰妮的“永久监护人(Conservator)”。根据美国法律,“监护(Conservatorship)”是一种将个体的个人、经济和法律决策权转让给他人的机制。对于因年老痴呆、身体或精神缺陷而无法自主作出决定的成年人,法院可以任命一位法定监护人,负责监督他的日常活动,如医疗保健或生活安排,并对它的财务事务进行托管。

此后13年,布兰妮便一直处于被父亲“监护”的状态之下。尽管在此期间,她发行了4张专辑,其中2张成为白金唱片(在美国,专辑销量超过100万张即可被认证为“白金唱片”),并在2018年进行了全球巡回演出,但法院仍判定,“她的精神状态无法自主作出明智的决定”。

在社交网络上,由布兰妮粉丝发起的“释放布兰妮(Free Britney)”运动已进行多年。自2020年起,布兰妮本人也试图通过法律途径摆脱被父亲监护的状态。

而这一次,布兰妮本人通过连线出庭,声泪俱下地讲述了自己这13年的遭遇。这不仅牵动了成千上万关注她的粉丝的心,也让更多粉丝之外的法律研究者、妇女权利保护者乃至美国国会议员开始反思“监护人”制度的正当性。

同时,布兰妮的遭遇也引发了不少好莱坞工作者的共情。在他们看来,是娱乐圈长期盛行的厌女风气,加之狗仔娱记和大众审视的步步紧逼,共同造成了布兰妮的悲剧。

为什么身为成年人、具有基本工作能力的布兰妮还要被“监护”?是什么让她被判定为“精神失常”,被媒体和舆论塑造为一个“疯女人”?谁该为布兰妮的悲剧负责?本文系统梳理了国内外多家媒体以及相关领域专家的各方观点。

撰文 | 肖舒妍

备受争议的监护权:是保护还是虐待?

“如果我能工作、赚钱、还能付钱给别人,我就不应该被监护,”在6月23日的庭审中,布兰妮提出了这一观点,“这毫无意义。法律需要改变。”

最初,布兰妮是如何“陷入”监护之中的?

2007年,布兰妮的状态一度滑入低谷。饱受舆论压力的她先是在一家理发店公开剃光了自己的头发,对着镜头大喊“我受够了别人不停碰我!”紧接着又陷入了和前夫凯文·费德勒争夺两个孩子抚养权的官司。她失去了孩子的独立抚养权,仅获得探视权。在一次探望孩子却被前夫拒之门外之后,面对狗仔的长枪短炮和不断追问,布兰妮情绪崩溃,举起一把雨伞企图赶走狗仔,却被狗仔拍下了她失控的状态,放上杂志封面。一时间,布兰妮“陷入疯癫”、成为“疯女人”的传闻便不绝于耳。

2007年2月,布兰妮情绪失控之后,在理发店公开剃光了自己的头发。

次年一月的一个晚上,费德勒来到布兰妮家中,准备接走两个孩子,却发现布兰妮把自己和儿子锁在卫生间,不肯开门。警方赶到后,发现布兰妮“受到精神类药物影响”,于是将其强制送往精神医疗中心治疗。在她住院期间,杰米·斯皮尔斯向法院提交申请,获得了女儿的“临时监护权”。在2008年10月,“临时监护权”又转为“永久监护权”。

尽管此后布兰妮的精神状态有所好转,监护托管却并未因此终止。熟悉此案的律师Vivian Thoreen在《纽约时报》拍摄的纪录片《陷害布兰妮》(Framing Britney Spears)中提到,如果被监护人想要终止监护,需要向法院提交一份请愿书,并提供自己已不需要监护(或监护已经失效)的证据,但在她所参与的所有案件中,没有一个被监护人曾成功终止监护,“一旦进入监护系统,就很难再有可能脱身”。

“如果个人想要终止监护,责任应该由反对终止的一方承担(以证明监护有必要继续),但实际上,通常情况下,往往是个人必须证明他们不再需要被监护。”卡多佐法学院(Cardozo School of law)的临床法学教授、监护法专家莱斯利·萨尔兹曼(Leslie Salzman)这样表示。

“就像‘第22条军规’的规定,疯子可以免于飞行任务,但必须由飞行员本人提出申请,而本人一旦提出申请,便可证明他并不是疯子。想从监护中脱身,布兰妮必须要证明自己有管理生活以及财产的能力,但她正处于监护中,也就意味着她没有上述能力。” 支持布兰妮的导演史塔克向《综艺》杂志说道。

一个精神正常的人该如何证明自己精神正常呢?如果布兰妮在监护状态下身心健康,则可以解释为“监护制度行之有效”;如果布兰妮对监护状态挣扎抵抗,又可以说明“她无法做出理智判断,必须接受监护”;在监护状态下,布兰妮犯的任何一个小错误,都能够作为她不宜解除监护的证据。但即使一个身心健全的普通人,也并不总能做出符合个人最大利益的明智决定。

“如果一个普通人决定,‘我要休假,喝几杯酒,吃一大堆甜甜圈,然后小睡一会儿,’这是很正常的事情,属于个人基本权利,”记者Sara Luterman举例论证,“但如果你有精神疾病或生理缺陷,这就会被视为你‘没有能力负责任地管理自己的生活’的进一步证据。”

在2020年的庭审中,法官在判决时提出,布兰妮的情况很特殊,她是一名“具有高能力的受监护人”。许多人对此感到疑惑,这显然是个自相矛盾的词。

另一方面,布兰妮的监护人人选也饱受质疑。杰米·斯皮尔斯虽然是布兰妮的父亲,却在2008年之前长期缺席她的人生,与妻子离婚多年,甚至找不到一张他和女儿合影。此外,他曾因酗酒被送进强制康复机构,多次创业失败最终申请破产。在纪录片《陷害布兰妮》中,布兰妮曾经的唱片营销总监回忆:“我不能判断杰米是个怎样的人,我只见过他一面。他对我说过唯一的一句话就是:‘我女儿会变得很富有,她会给我买艘大船。’”

布兰妮的父亲杰米·斯皮尔斯。(图源:纪录片《陷害布兰妮》)

无论是布兰妮的母亲林恩(Lynn),还是自她5岁起就认识她、之后一直陪伴她参加活动、帮助她打理事业的长期助理Felicia Culotta,都比杰米更加了解、关心布兰妮的生活状态。在进入监护状态之后,布兰妮曾多次向法院提出更换监护人,在2020年更表示,如果杰米继续掌控她的事业,她将拒绝演出。

“虽然受监护人未必能够选择自己的监护人,但他们当然可以推荐并说明希望谁成为监护人。法院应该对这一请求给予充分考虑。考虑到监护人要托管受监护人的个人事务,他要能够被对方所接受。”萨尔兹曼这样认为。而在布兰妮的案例中,她的需求显然没有被尊重。

布兰妮的支持者要求法院终止她的“被监护”状态。

加之布兰妮作为全球巨星所拥有的高额财产,进一步提高了监护权的复杂程度。布兰妮的公开资产为6000万美元,本身极为可观,同时有媒体披露,布兰妮的实际财产超过6亿美元,但大部分均被杰米转移。此外,布兰妮还要提供自己全部收入的1.5%作为“监护佣金”。很难判断对杰米而言,女儿是需要照顾的受监护人,还是日进斗金的摇钱树。

布兰妮在法庭陈述中提到,自己被要求每周工作7天,每天工作10小时,2018年的巡回演出和在拉斯维加斯长达4年的驻唱都是被迫签约,即便在发烧40度时她也要上台演出。根据《纽约时报》的调查,布兰妮在拉斯维加斯的演出总票房高达1.37亿美元,但属于布兰妮的分成只有每周2000美元。

而在2020年接受法院问询时,杰米团队的律师更提出,他们将采用一种“全新的混合型商业模式”,帮助布兰妮完成事业开拓和财富增长。有团队表示,监护团队成立时,布兰妮的资产只有几百万美元,而现在已超过6000万美元,这证明了团队的监护工作行之有效。但他们似乎没有意识到,托管成立的初衷,是为了帮助布兰妮恢复身心健康,而不是让她成为赚钱机器。

在布兰妮与杰米对簿公堂时,她不仅要支付自己的治疗费用、自己的法务费用,还要支付杰米团队的工资,杰米方的法务费用。

杰奎琳·布彻(Jacqueline Butcher)曾是斯皮尔斯家族的好友,并在2008年提供证词帮助杰米获得监护权。而现在,她对自己过去的行为表示遗憾,“当时我以为自己是在帮忙,但实际上我帮一个腐败的家族掌控了一切。”

“尽管布兰妮的情况看起来非常极端并令人不安,” Erica Schwiegershausen在《纽约》杂志中写道,“她讲述的大部分内容——例如未经同意接受药物治疗、接受非自愿精神病评估和被强制送进精神病院——对任何有精神疾病经历的人来说都很熟悉。”据估计,在美国有数以百万计的智力缺陷或社交能力缺陷者被剥夺了法律行为能力,处于某种形式的监护之下。这导致了一系列虐待行为,包括强制医疗、强制避孕、强制终止妊娠、非自愿监禁、强制生活安排和行动自由受限。

纪录片《陷害布兰妮》剧照。

父权控制与女性反抗:一场漫长的斗争

在布兰妮所遭受的一系列限制和虐待中,“强制避孕”一点尤其令人不解,无论从任何角度都无法看作是对于布兰妮的保护。在法庭陈述中,布兰妮说道:“我希望能够结婚生子,但这却被监护团队所禁止。我的体内放有宫内节育器,因此无法怀孕。我想取出宫内节育器,再生一个孩子,可是这个所谓的团队不让我接近医生。”

限制布兰妮生育这一行为,几乎遭到了美国各党派人士的谴责,包括美国计划生育行动基金董事会主席亚历克西斯·麦吉尔·约翰逊(Alexis McGill Johnson),保守派共和党参议员特德克鲁兹(Ted Cruz)和众议员南希·梅斯(Nancy Mace)、卡罗琳·马洛尼(Carolyn B.Maloney)。

“你可以打着保护的幌子强迫一个女人绝育,这太疯狂了。如果这能发生在布兰妮·斯皮尔斯身上,全国还会有多少其他女人在默默受苦?” 众议员南希·梅斯在推特上写道。

“从一开始,男人就控制着女人的身体、思想和抱负,”另一位众议员卡罗琳·马洛尼则向《华盛顿邮报》表示,“斯皮尔斯女士和世界各地的任何女性一样,理应对自己的身体、权益和财产享有完全的自主权。”

这一控制在布兰妮身上显得尤其让人心碎。在出道伊始,布兰妮的形象便与独立、自主、强大联系在一起。在早年的采访视频中,她笑着告诉记者:“我知道自己所有的合同,我知道自己所有要做的工作,我才不是那种只听经纪人话的女孩。”

曾与布兰妮合作过的伴舞也多次提到:“她绝不是牵线木偶。她是老大(boss)。”对于演出造型、舞蹈动作、演唱歌曲,布兰妮都会坚定而自信地提出自己的看法。

在粉丝看来,布兰妮展示出的“女人的我行我素,不必再取悦他人”,正是她最大的魅力所在。“布兰妮的形象不再只是父权制下少女形象的意义典范,而是父权控制与女性抵抗,甚至资本主义与个体之间的符号冲突(semiotic struggle)的表现。”公众号“看理想”在一篇文章中如此概括布兰妮的文化意义。

但是当时的美国大众,一方面狂热地喜爱这样一个“既性感又纯洁、既乖巧又独立”的荧幕形象,一方面却又没做好准备接受这样的女性走出荧幕、走进生活。“在‘圣经地带’出生、长大的布兰妮,身上集中了美国人的两种期望:既要穿着性感,给人暧昧的想象,又要保持纯洁的处女身;既要尽可能地触探性的界限,又绝不能越雷池半步。两种期望显然是矛盾的,这让她受到了严格的形象、行为、道德审查。” 作者李孟苏在“三联生活周刊”公众号文章中写道。

纪录片《陷害布兰妮》截图。

当时美国马里兰州的州长夫人肯德尔·埃尔利希(Kendel Ehrlich)甚至公开对媒体表示:“如果我有机会,真想一枪崩了布兰妮。”以此指责布兰妮给孩子们做了不良的榜样,让美国的妈妈们感到不安。

在一次访谈中,主持人当着布兰妮的面播放了这段视频,布兰妮听到后紧蹙双眉,微微摇头,“太可怕了……可是,我不是用来帮她们教育小孩的啊。”她忍不住哭了出来。

在纪录片《陷害布兰妮》中,一位业内人士提出,没有一个男孩乐队的成员会像布兰妮这样受到如此严格的道德审查。

“让我们直截了当地说:发生在布兰妮·斯皮尔斯身上的事情,永远不会发生在一个男版的她身上。”专栏作家海琳·奥伦(Helaine Olen)在《华盛顿邮报》一针见血地写道,“想想看,有多少男明星曾在公共场合做出过疯狂的、情绪失控的、甚至磕了药似的行为。几秒内我就能说出迈克尔·杰克逊、坎耶·韦斯特和小罗伯特·唐尼的名字。小罗伯特·唐尼曾被警察带走,理由是他光着身子走进邻居家,躺在孩子的床上睡着了。当邻居拨打911时,你甚至可以听到他的鼾声。还有布兰妮的父亲,杰米——一个非常不合时宜的男人,布兰妮的前夫曾申请并获得了限制令,禁止杰米靠近自己以及两个孩子。”

“管理学理论表明,人们会默认男人是有能力的,不管他们的过去如何;而女性却不得不一次又一次证明自己的工作能力、财务敏感、理智水平。” 海琳·奥伦继续写道,“纵观历史,监护制度和非自愿承诺制度一直被用来控制女性的人身自由和财务自由。”

纪录片《陷害布兰妮》截图。一位曾经合作过的演员在接受采访时如此评论布兰妮。

回看当时舆论对布兰妮的围观,以及对她“精神病”的指控,都弥漫着浓厚的厌女气息。莫伊拉·多纳根(Moira Donegan)在《卫报》写道:“历史上,许多人仅仅因为持有不受欢迎的观点,或行为冒犯了普遍传统,就被认为是疯了。女性尤其深受其害。长期以来,那些厌恶女性或觊觎女性财富的人,总能因为一些微不足道的原因,把女性指控成疯子。”

而在纪录片《陷害布兰妮》于今年2月5日上线之后,联合国妇女署活动家蒙罗·伯格多夫(Munroe Bergdorf)在社交媒体上严肃表态:“布兰妮为自己所取得的成就付出了太大的代价。当时的社会选择对精神疾病避而不谈,不愿正视女性的自我风貌,不知如何消解四处蔓延的厌女情绪。女性公众人物被媒体拿来消费。世人追捧你到制高点,为的是最终能亲手将你毁灭。”

大众审视与娱乐产业:谁是施害者,谁是受害者?

“世人追捧你到制高点,为的是最终能亲手将你毁灭。”这句话,可能道出了布兰妮悲剧的本质。

首张个人专辑《…Baby One More Time》让她一夜成名,全球销量超过3000万张,成为世界销量最高的专辑之一。但水能载舟亦能覆舟,随着喜爱而来的是大众对她从头到脚的审视,以及对她私生活永不餍足的窥探。

布兰妮的首张个人专辑《…Baby One More Time》封面。

在布兰妮10岁刚出道的一段视频中,一位满头银发的主持人问身高才到自己腰身的布兰妮:“你有男朋友了吗?……你可以考虑我。”而当时的观众,并没有人感到不适。

年龄稍长之后,布兰妮遇到的采访问题更加刻薄露骨:“你还是处女吗?”“你知道所有人都在想着你的胸吗?”而在2001年的一段电视采访中(当时布兰妮只有19岁),主持人直言:“对许多人来说,你是一个矛盾体。一面是甜美、纯洁、童贞,一面是只穿着内衣的性感荡妇。”布兰妮只好尴尬回应:“不是‘只穿着内衣’,在《滚石》的封面上是唯一一次。我在演出时不会。”

摄影师大卫·拉查佩尔(David LaChapelle)拍摄的这张著名照片登在了《滚石》的封面上,让布兰妮成为被公众凝视的对象。

一张布兰妮的“黑照”,价值100万美元,在纪录片《陷害布兰妮》中职业狗仔记者Daniel Ramos提到。重赏之下,必有勇夫。于是布兰妮的私人生活也暴露在了长枪短炮、层层包围的镜头之下。

在前文提及的布兰妮在前夫家探视孩子未果后,正是Daniel Ramos步步紧逼、追问布兰妮的心情,最终致使她情绪崩溃,挥舞雨伞砸向了他开来的车。在拍下了布兰妮的失态照片后,他摸着车上坑坑洼洼的痕迹,喜笑颜开:“天啊,这次赚大了。”凭借几次三番偷拍到布兰妮形容憔悴、情绪失控或是剃成光头的照片,Daniel Ramos赚得盆满钵满。

多年以后,《纽约时报》纪录片团队问这名娱乐记者:“你觉得自己影响了布兰妮的生活吗?”

Daniel Ramos回答:“我不这么认为。在她身边工作了这么多年,她从来没对我们说‘我不想理你们,别烦我了’。”

团队反问:“她说过‘别烦我’吧?”

Daniel Ramos想了片刻,回答道:“她是说过‘今天你能让我一个人待着吗?’,但并不代表说‘永远别烦我’啊。”

纪录片《陷害布兰妮》截图。直播采访时,主持人问布兰妮:“你是不是希望那些狗仔队都走开?”

比起布兰妮巅峰时期温暖阳光的笑容,她低谷阶段剃光的头发、浓重的黑眼圈似乎更能勾起大众的注意、引发人们的唏嘘。

《每日人物》主笔安小庆曾把布兰妮的境遇与她笔下香港的“疯女人们”进行类比。在《香港为什么有那么多“疯女人”? 》一文中,她写道:

蓝洁瑛顶着“四大癫王“的封号,与后来的“疯女人“们——吴绮莉、吴卓林、关淑怡一起,以肉身的磨蚀和精神的苦痛,源源不断地给香港社会供应着日常运转所需的“疯癫“样本。比起“传奇“,他们更热衷狗血和疯癫,尤其是那些从原有高阶层跌落折堕的“疯女人“的故事,因为这不仅能够满足世人猎奇的心态,还能最大程度地警示所有的香港人、尤其是香港女人——在这个国际自由贸易港,这个顶级消费社会,贫穷是可耻的,阶层坠落更是不可饶恕的。

2021年3月,布兰妮·斯皮尔斯在社交媒体上发了一段跳舞的视频,配文提到,在《陷害布兰妮》上线后,她足足“哭了两个星期”,“我的人生总是被猜测、被注视、被审判。为了我的精神状态(保持理智和稳定),我需要在每个晚上跳舞,来感受野性地、作为一个人似的活着。我的整个人生都被表演在公众面前。将脆弱托付给世界、展现给世界,真的需要很大的力量,因为我总是被审判、被侮辱,被媒体搞得狼狈不堪,直到今天依然如此。”

在《新周刊》的记者眼中,纪录片《陷害布兰妮》是对过去伤害布兰妮的小报的一次清算,人们希望借此将这位昔日美国偶像从疑似泥沼般的生活里解救出来。

但很难判断,布兰妮自己是否需要媒体的再一次巨大关注。对她而言,《陷害布兰妮》到底是对她的拯救,还是再一次揭开她的疤痕?就像粉丝发起的“释放布兰妮”运动,通过布兰妮社交媒体上的蛛丝马迹来解读她的生活状态,从而得出她需要“被拯救”的结论,这一方面让更多粉丝联合起来,给予布兰妮支持和力量,另一方面,也让布兰妮的隐私和伤痛暴露于公众面前。

纪录片《陷害布兰妮》截图。

“世界根本无权知道有关布兰妮·斯皮尔斯的任何事情。但是,大众和媒体却喜欢对女性公众人物生活中最私密的细节进行关注和判断。每一个粉丝、记者和媒体评论员都认为自己有权了解和评判斯皮尔斯最私人的事情。这是一种令人不安的侵犯,和监护本身带来的控制似乎并没有什么不同,”霍夫斯特拉大学(Hofstra University)公共关系副教授卡拉·阿拉莫(Kara Alaimo)发表在CNN的评论中写道,“监护是否必要,是围绕布兰妮·斯皮尔斯的心理健康和财务状况展开的。这些都是她有权保密的问题。在美国,关于这方面的隐私权规定非常明确。《纽约时报》和其他新闻媒体的报道并不违法,但很难把挖掘女性私生活理解为为了什么公共利益。”

这意味着,在屏幕之前审视、关注甚至支持着布兰妮的人们也并非全然置身事外,以至于在某种程度上成为了悲剧的帮凶。公众号“看理想”则进一步提出,这种审视不仅伤害了布兰妮,也可能在伤害我们,在观看明星被规训的同时,我们自身也成了规训的对象——

如果身为“监视主体”的我们,总是在以自我感觉良好的傲慢态度和侵略性眼光来检视着这些明星的话,也会在不知不觉间通过自我规训的方式,默然顺从了偏见、刻板和节目所宣扬的美学标准与行为规范。——想要安全逃脱这种“全景敞视机制”(panopticism)的控制,是绝无可能的。

参考链接:

1.纪录片《陷害布兰妮》(Framing Britney Spears),《纽约时报》团队拍摄

2.https://www.forbes.com/sites/maddieberg/2021/06/23/britney-spears-full-statement-against-her-conservatorship/?sh=4cb8fb0421bd

3.https://www.npr.org/2021/06/24/1009726455/britney-spears-conservatorship-how-thats-supposed-to-work

4.https://www.nytimes.com/2021/07/01/us/politics/britney-spears-warren-casey-conservatorship.html

5.https://www.foxnews.com/entertainment/free-britney-a-bipartisan-cause-on-capitol-hill-after-impassioned-testimony

6.https://www.hrw.org/news/2021/06/26/britney-spearss-conservatorship-mirrors-reality-millions-disabilities

7.https://www.cnn.com/2021/06/23/opinions/britney-spears-deserves-privacy-alaimo/index.html

8. https://www.nytimes.com/2021/06/29/opinion/britney-spears-conservatorship.html

9.https://www.washingtonpost.com/opinions/2021/06/26/britney-spears-conservatorship-mistreatment/

10.《是谁陷害了“小甜甜”布兰妮?》,看理想,2021-02-25,https://mp.weixin.qq.com/s/Cg02JcHn4uOpKWqEEhosKA

11.《从小甜甜到“疯女人”,谁在陷害布兰妮?》,新周刊,2021-05-02,https://mp.weixin.qq.com/s/dhFRiUXByvhgvwCqFTcPrA

12.《谁制造了小甜甜布兰妮的悲剧?》,李孟苏,三联生活周刊,2021-03-10,https://mp.weixin.qq.com/s/G4_VB4xBSSTzET-qYsqgFw

13.《香港为什么有那么多“疯女人”?》,安小庆,每日人物,2018-08-19,https://mp.weixin.qq.com/s/K5fK_TIkH5VrTClPjBMG8g

撰文 | 肖舒妍

编辑 | 李永博;王青

校对 | 王心

端开发者丨JavaScript

实际需求中开始

要求:

  • 此类继承自 Date,拥有Date的所有属性和对象

  • 此类可以自由拓展方法

形象点描述,就是要求可以这样:

  1. // 假设最终的类是 MyDate,有一个getTest拓展方法

  2. let date=newMyDate();

  3. // 调用Date的方法,输出GMT绝对毫秒数

  4. console.log(date.getTime());

  5. // 调用拓展的方法,随便输出什么,譬如helloworld!

  6. console.log(date.getTest());

于是,随手用JS中经典的组合寄生法写了一个继承,然后,刚准备完美收工,一运行,却出现了以下的情景:

但是的心情是这样的: 囧

以前也没有遇到过类似的问题,然后自己尝试着用其它方法,多次尝试,均无果(不算暴力混合法的情况),其实回过头来看,是因为思路新奇,凭空想不到,并不是原理上有多难。。。

于是,借助强大的搜素引擎,搜集资料,最后,再自己总结了一番,才有了本文。

正文开始前,各位看官可以先暂停往下读,尝试下,在不借助任何网络资料的情况下,是否能实现上面的需求?(就以 10分钟为限吧)

分析问题的关键

借助stackoverflow上的回答。

经典的继承法有何问题

先看看本文最开始时提到的经典继承法实现,如下:

  1. /**

  2. * 经典的js组合寄生继承

  3. */

  4. functionMyDate() {

  5. Date.apply(this, arguments);

  6. this.abc=1;

  7. }

  8. functioninherits(subClass, superClass) {

  9. functionInner() {}

  10. Inner.prototype=superClass.prototype;

  11. subClass.prototype=newInner();

  12. subClass.prototype.constructor=subClass;

  13. }

  14. inherits(MyDate,Date);

  15. MyDate.prototype.getTest=function() {

  16. returnthis.getTime();

  17. };

  18. let date=newMyDate();

  19. console.log(date.getTest());

  20. 就是这段代码?,这也是JavaScript高程(红宝书)中推荐的一种,一直用,从未失手,结果现在马失前蹄。。。

    我们再回顾下它的报错:

    再打印它的原型看看:

    怎么看都没问题,因为按照原型链回溯规则, Date的所有原型方法都可以通过 MyDate对象的原型链往上回溯到。再仔细看看,发现它的关键并不是找不到方法,而是 thisisnotaDateobject.

    嗯哼,也就是说,关键是:由于调用的对象不是Date的实例,所以不允许调用,就算是自己通过原型继承的也不行。

    为什么无法被继承?

    首先,看看 MDN上的解释,上面有提到,JavaScript的日期对象只能通过 JavaScriptDate作为构造函数来实例化。

    然后再看看stackoverflow上的回答:

    有提到, v8引擎底层代码中有限制,如果调用对象的 [[Class]]不是 Date,则抛出错误。

    总的来说,结合这两点,可以得出一个结论:要调用Date上方法的实例对象必须通过Date构造出来,否则不允许调用Date的方法。

    该如何实现继承?

    虽然原因找到了,但是问题仍然要解决啊,真的就没办法了么?当然不是,事实上还是有不少实现的方法的。

    暴力混合法

    首先,说说说下暴力的混合法,它是下面这样子的:

    说到底就是:内部生成一个 Date对象,然后此类暴露的方法中,把原有 Date中所有的方法都代理一遍,而且严格来说,这根本算不上继承(都没有原型链回溯)。

    ES5黑魔法

    然后,再看看ES5中如何实现?

    1. // 需要考虑polyfill情况

    2. Object.setPrototypeOf=Object.setPrototypeOf ||

    3. function(obj, proto) {

    4. obj.__proto__=proto;

    5. returnobj;

    6. };

    7. /**

    8. * 用了点技巧的继承,实际上返回的是Date对象

    9. */

    10. functionMyDate() {

    11. // bind属于Function.prototype,接收的参数是:object, param1, params2...

    12. vardateInst=new(Function.prototype.bind.apply(Date, [Date].concat(Array.prototype.slice.call(arguments))))();

    13. // 更改原型指向,否则无法调用MyDate原型上的方法

    14. // ES6方案中,这里就是[[prototype]]这个隐式原型对象,在没有标准以前就是__proto__

    15. Object.setPrototypeOf(dateInst,MyDate.prototype);

    16. dateInst.abc=1;

    17. returndateInst;

    18. }

    19. // 原型重新指回Date,否则根本无法算是继承

    20. Object.setPrototypeOf(MyDate.prototype,Date.prototype);

    21. MyDate.prototype.getTest=functiongetTest() {

    22. returnthis.getTime();

    23. };

    24. let date=newMyDate();

    25. // 正常输出,譬如1515638988725

    26. console.log(date.getTest());

    27. 一眼看上去不知所措?没关系,先看下图来理解:(原型链关系一目了然)

      可以看到,用的是非常巧妙的一种做法:

      正常继承的情况如下:

      • newMyDate()返回实例对象 date是由 MyDate构造的

      • 原型链回溯是: date(MyDate对象)-date.__proto__-MyDate.prototype-MyDate.prototype.__proto__-Date.prototype

      这种做法的继承的情况如下:

      • newMyDate()返回实例对象 date是由 Date构造的

      • 原型链回溯是: date(Date对象)-date.__proto__-MyDate.prototype-MyDate.prototype.__proto__-Date.prototype

      可以看出,关键点在于:

      • 构造函数里返回了一个真正的 Date对象(由 Date构造,所以有这些内部类中的关键 [[Class]]标志),所以它有调用 Date原型上方法的权利

      • 构造函数里的Date对象的 [[ptototype]](对外,浏览器中可通过 __proto__访问)指向 MyDate.prototype,然后 MyDate.prototype再指向 Date.prototype。

      所以最终的实例对象仍然能进行正常的原型链回溯,回溯到原本Date的所有原型方法。

      这样通过一个巧妙的欺骗技巧,就实现了完美的Date继承。不过补充一点, MDN上有提到尽量不要修改对象的 [[Prototype]],因为这样可能会干涉到浏览器本身的优化。如果你关心性能,你就不应该在一个对象中修改它的 [[Prototype]]

      ES6大法

      当然,除了上述的ES5实现,ES6中也可以直接继承(自带支持继承 Date),而且更为简单:

      1. classMyDateextendsDate{

      2. constructor() {

      3. super();

      4. this.abc=1;

      5. }

      6. getTest() {

      7. returnthis.getTime();

      8. }

      9. }

      10. let date=newMyDate();

      11. // 正常输出,譬如1515638988725

      12. console.log(date.getTest());

      对比下ES5中的实现,这个真的是简单的不行,直接使用ES6的Class语法就行了。而且,也可以正常输出。

      注意:这里的正常输出环境是直接用ES6运行,不经过babel打包,打包后实质上是转化成ES5的,所以效果完全不一样。

      ES6写法,然后Babel打包

      虽然说上述ES6大法是可以直接继承Date的,但是,考虑到实质上大部分的生产环境是: ES6+Babel

      直接这样用ES6 + Babel是会出问题的。

      不信的话,可以自行尝试下,Babel打包成ES5后代码大致是这样的:

      然后当信心满满的开始用时,会发现:

      对,又出现了这个问题,也许这时候是这样的⊙?⊙

      因为转译后的ES5源码中,仍然是通过 MyDate来构造,而 MyDate的构造中又无法修改属于 Date内部的 [[Class]]之类的私有标志,因此构造出的对象仍然不允许调用 Date方法(调用时,被引擎底层代码识别为 [[Class]]标志不符合,不允许调用,抛出错误)。

      由此可见,ES6继承的内部实现和Babel打包编译出来的实现是有区别的。(虽说Babel的polyfill一般会按照定义的规范去实现的,但也不要过度迷信)。

      几种继承的细微区别

      虽然上述提到的三种方法都可以达到继承 Date的目的-混合法严格说不能算继承,只不过是另类实现。

      于是,将所有能打印的主要信息都打印出来,分析几种继承的区别,大致场景是这样的:

      可以参考:( 请进入调试模式)https://dailc.github.io/fe-interview/demo/extends_date.html

      从上往下, 1,2,3,4四种继承实现分别是:(排出了混合法)

      • ES6的Class大法

      • 经典组合寄生继承法

      • 本文中的取巧做法,Date构造实例,然后更改 __proto__的那种

      • ES6的Class大法,Babel打包后的实现(无法正常调用的)

      1. ~~~~以下是MyDate们的prototype~~~~~~~~~

      2. Date{constructor: ?, getTest: ?}

      3. Date{constructor: ?, getTest: ?}

      4. Date{getTest: ?, constructor: ?}

      5. Date{constructor: ?, getTest: ?}

      6. ~~~~以下是new出的对象~~~~~~~~~

      7. SatJan13201821:58:55GMT+0800(CST)

      8. MyDate2{abc:1}

      9. SatJan13201821:58:55GMT+0800(CST)

      10. MyDate{abc:1}

      11. ~~~~以下是new出的对象的Object.prototype.toString.call~~~~~~~~~

      12. [objectDate]

      13. [objectObject]

      14. [objectDate]

      15. [objectObject]

      16. ~~~~以下是MyDate们的__proto__~~~~~~~~~

      17. ?Date() { [native code] }

      18. ? () { [native code] }

      19. ? () { [native code] }

      20. ?Date() { [native code] }

      21. ~~~~以下是new出的对象的__proto__~~~~~~~~~

      22. Date{constructor: ?, getTest: ?}

      23. Date{constructor: ?, getTest: ?}

      24. Date{getTest: ?, constructor: ?}

      25. Date{constructor: ?, getTest: ?}

      26. ~~~~以下是对象的__proto__与MyDate们的prototype比较~~~~~~~~~

      27. true

      28. true

      29. true

      30. true

      31. 看出,主要差别有几点:

        1. MyDate们的proto指向不一样

        2. Object.prototype.toString.call的输出不一样

        3. 对象本质不一样,可以正常调用的 1,3都是 Date构造出的,而其它的则是 MyDate构造出的

        我们上文中得出的一个结论是:由于调用的对象不是由Date构造出的实例,所以不允许调用,就算是自己的原型链上有Date.prototype也不行

        但是这里有两个变量:分别是底层构造实例的方法不一样,以及对象的 Object.prototype.toString.call的输出不一样(另一个 MyDate.__proto__可以排除,因为原型链回溯肯定与它无关)。

        万一它的判断是根据 Object.prototype.toString.call来的呢?那这样结论不就有误差了?

        于是,根据ES6中的, Symbol.toStringTag,使用黑魔法,动态的修改下它,排除下干扰:

        1. // 分别可以给date2,date3设置

        2. Object.defineProperty(date2,Symbol.toStringTag, {

        3. get:function() {

        4. returnDate;

        5. }

        6. });

        然后在打印下看看,变成这样了:

        1. [objectDate]

        2. [objectDate]

        3. [objectDate]

        4. [objectObject]

        可以看到,第二个的 MyDate2构造出的实例,虽然打印出来是 [objectDate],但是调用Date方法仍然是有错误。

        此时我们可以更加准确一点的确认:由于调用的对象不是由Date构造出的实例,所以不允许调用。

        而且我们可以看到,就算通过黑魔法修改 Object.prototype.toString.call,内部的 [[Class]]标识位也是无法修改的。(这块知识点大概是Object.prototype.toString.call可以输出内部的[[Class]],但无法改变它,由于不是重点,这里不赘述)。

        ES6继承与ES5继承的区别

        从上午中的分析可以看到一点:ES6的Class写法继承是没问题的。但是换成ES5写法就不行了。

        所以ES6的继承大法和ES5肯定是有区别的,那么究竟是哪里不同呢?(主要是结合的本文继承Date来说)

        区别:(以 SubClass, SuperClass, instance为例)

        ES5中继承的实质是:(那种经典组合寄生继承法)

        • 先由子类( SubClass)构造出实例对象this

        • 然后在子类的构造函数中,将父类( SuperClass)的属性添加到 this上, SuperClass.apply(this,arguments)

        • 子类原型( SubClass.prototype)指向父类原型( SuperClass.prototype)

        • 所以 instance是子类( SubClass)构造出的(所以没有父类的 [[Class]]关键标志)

        • 所以, instance有 SubClass和 SuperClass的所有实例属性,以及可以通过原型链回溯,获取 SubClass和 SuperClass原型上的方法

        ES6中继承的实质是:

        • 先由父类( SuperClass)构造出实例对象this,这也是为什么必须先调用父类的 super()方法(子类没有自己的this对象,需先由父类构造)

        • 然后在子类的构造函数中,修改this(进行加工),譬如让它指向子类原型( SubClass.prototype),这一步很关键,否则无法找到子类原型(注,子类构造中加工这一步的实际做法是推测出的,从最终效果来推测)

        • 然后同样,子类原型( SubClass.prototype)指向父类原型( SuperClass.prototype)

        • 所以 instance是父类( SuperClass)构造出的(所以有着父类的 [[Class]]关键标志)

        • 所以, instance有 SubClass和 SuperClass的所有实例属性,以及可以通过原型链回溯,获取 SubClass和 SuperClass原型上的方法

        以上?就列举了些重要信息,其它的如静态方法的继承没有赘述。(静态方法继承实质上只需要更改下 SubClass.__proto__到 SuperClass即可)

        可以看着这张图快速理解:

        有没有发现呢:ES6中的步骤和本文中取巧继承Date的方法一模一样,不同的是ES6是语言底层的做法,有它的底层优化之处,而本文中的直接修改_proto_容易影响性能。

        ES6中在super中构建this的好处?

        因为ES6中允许我们继承内置的类,如Date,Array,Error等。如果this先被创建出来,在传给Array等系统内置类的构造函数,这些内置类的构造函数是不认这个this的。所以需要现在super中构建出来,这样才能有着super中关键的 [[Class]]标志,才能被允许调用。(否则就算继承了,也无法调用这些内置类的方法)

        构造函数与实例对象

        看到这里,不知道是否对上午中频繁提到的构造函数,实例对象有所混淆与困惑呢?这里稍微描述下。

        要弄懂这一点,需要先知道 new一个对象到底发生了什么?先形象点说:

        new MyClass()中,都做了些什么工作
        1. functionMyClass() {

        2. this.abc=1;

        3. }

        4. MyClass.prototype.print=function() {

        5. console.log('this.abc:'+this.abc);

        6. };

        7. let instance=newMyClass();

        譬如,上述就是一个标准的实例对象生成,都发生了什么呢?

        步骤简述如下:(参考MDN,还有部分关于底层的描述略去-如[[Class]]标识位等)

        1. 构造函数内部,创建一个新的对象,它继承自 MyClass.prototype, letinstance=Object.create(MyClass.prototype);

        2. 使用指定的参数调用构造函数 MyClass,并将 this绑定到新创建的对象, MyClass.call(instance);,执行后拥有所有实例属性

        3. 如果构造函数返回了一个“对象”,那么这个对象会取代整个 new出来的结果。如果构造函数没有返回对象,那么new出来的结果为步骤1创建的对象。 (一般情况下构造函数不返回任何值,不过用户如果想覆盖这个返回值,可以自己选择返回一个普通对象来覆盖。当然,返回数组也会覆盖,因为数组也是对象。)

        结合上述的描述,大概可以还原成以下代码(简单还原,不考虑各种其它逻辑):

        1. let instance=Object.create(MyClass.prototype);

        2. let innerConstructReturn=MyClass.call(instance);

        3. let innerConstructReturnIsObj=typeofinnerConstructReturn==='object'||typeofinnerConstructReturn==='function';

        4. returninnerConstructReturnIsObj ? innerConstructReturn : instance;

        注意?:普通的函数构建,可以简单的认为就是上述步骤。实际上对于一些内置类(如Date等),并没有这么简单,还有一些自己的隐藏逻辑,譬如 [[Class]]标识位等一些重要私有属性。譬如可以在MDN上看到,以常规函数调用Date(即不加 new 操作符)将会返回一个字符串,而不是一个日期对象,如果这样模拟的话会无效。

        觉得看起来比较繁琐?可以看下图梳理:

        那现在再回头看看。

        什么是构造函数?

        如上述中的 MyClass就是一个构造函数,在内部它构造出了 instance对象。

        什么是实例对象?

        instance就是一个实例对象,它是通过 new出来的?

        实例与构造的关系

        有时候浅显点,可以认为构造函数是xxx就是xxx的实例。即:

        1. let instance=newMyClass();

        此时我们就可以认为 instance是 MyClass的实例,因为它的构造函数就是它。

        实例就一定是由对应的构造函数构造出的么?

        不一定,我们那ES5黑魔法来做示例。

        1. functionMyDate() {

        2. // bind属于Function.prototype,接收的参数是:object, param1, params2...

        3. vardateInst=new(Function.prototype.bind.apply(Date, [Date].concat(Array.prototype.slice.call(arguments))))();

        4. // 更改原型指向,否则无法调用MyDate原型上的方法

        5. // ES6方案中,这里就是[[prototype]]这个隐式原型对象,在没有标准以前就是__proto__

        6. Object.setPrototypeOf(dateInst,MyDate.prototype);

        7. dateInst.abc=1;

        8. returndateInst;

        9. }

        10. 我们可以看到 instance的最终指向的原型是 MyDate.prototype,而 MyDate.prototype的构造函数是 MyDate,因此可以认为 instance是 MyDate的实例。

          但是,实际上, instance却是由 Date构造的,我们可以继续用 ES6中的 new.target来验证。

          注意?:关于 new.target, MDN中的定义是:new.target返回一个指向构造方法或函数的引用。

          嗯哼,也就是说,返回的是构造函数。

          我们可以在相应的构造中测试打印:

          1. classMyDateextendsDate{

          2. constructor() {

          3. super();

          4. this.abc=1;

          5. console.log('~~~new.target.name:MyDate~~~~');

          6. console.log(new.target.name);

          7. }

          8. }

          9. // new操作时的打印结果是:

          10. // ~~~new.target.name:MyDate~~~~

          11. // MyDate

          然后,可以在上面的示例中看到,就算是ES6的Class继承, MyDate构造中打印 new.target也显示 MyDate,但实际上它是由 Date来构造(有着 Date关键的 [[Class]]标志,因为如果不是Date构造(如没有标志)是无法调用Date的方法的)。

          这也算是一次小小的勘误吧。

          所以,实际上用 new.target是无法判断实例对象到底是由哪一个构造构造的(这里指的是判断底层真正的 [[Class]]标志来源的构造)。

          再回到结论:实例对象不一定就是由它的原型上的构造函数构造的,有可能构造函数内部有着寄生等逻辑,偷偷的用另一个函数来构造了下,当然,简单情况下,我们直接说实例对象由对应构造函数构造也没错(不过,在涉及到这种Date之类的分析时,我们还是得明白)。

          [[Class]]与Internal slot

          这一部分为补充内容。

          前文中一直提到一个概念:Date内部的 [[Class]]标识。

          其实,严格来说,不能这样泛而称之(前文中只是用这个概念是为了降低复杂度,便于理解),它可以分为以下两部分:

          在ES5中,每种内置对象都定义了 [[Class]] 内部属性的值,[[Class]] 内部属性的值用于内部区分对象的种类

          • Object.prototype.toString访问的就是这个[[Class]]

          • 规范中除了通过 Object.prototype.toString,没有提供任何手段使程序访问此值。

          • 而且Object.prototype.toString输出无法被修改

          而在ES5中,之前的 [[Class]] 不再使用,取而代之的是一系列的 internalslot

          • Internal slot 对应于与对象相关联并由各种ECMAScript规范算法使用的内部状态,它们没有对象属性,也不能被继承

          • 根据具体的 Internal slot 规范,这种状态可以由任何ECMAScript语言类型或特定ECMAScript规范类型值的值组成

          • 通过 Object.prototype.toString,仍然可以输出Internal slot值

          • 简单点理解(简化理解),Object.prototype.toString的流程是:如果是基本数据类型(除去Object以外的几大类型),则返回原本的slot,如果是Object类型(包括内置对象以及自己写的对象),则调用 Symbol.toStringTag。 Symbol.toStringTag方法的默认实现就是返回对象的Internal slot,这个方法可以被重写

          这两点是有所差异的,需要区分(不过简单点可以统一理解为内置对象内部都有一个特殊标识,用来区分对应类型-不符合类型就不给调用)。

          JS内置对象是这些:

          1. Arguments,Array,Boolean,Date,Error,Function,JSON,Math,Number,Object,RegExp,String

          ES6新增的一些,这里未提到:(如Promise对象可以输出 [objectPromise]),而前文中提到的:

          1. Object.defineProperty(date,Symbol.toStringTag, {

          2. get:function() {

          3. returnDate;

          4. }

          5. });

          它的作用是重写Symbol.toStringTag,截取date(虽然是内置对象,但是仍然属于Object)的 Object.prototype.toString的输出,让这个对象输出自己修改后的 [objectDate]。

          但是,仅仅是做到输出的时候变成了Date,实际上内部的 internalslot值并没有被改变,因此仍然不被认为是Date。

          如何快速判断是否继承?

          其实,在判断继承时,没有那么多的技巧,就只有关键的一点: [[prototype]]( __ptoto__)的指向关系。

          譬如:

          1. console.log(instanceinstanceofSubClass);

          2. console.log(instanceinstanceofSuperClass);

          实质上就是:

          • SubClass.prototype是否出现在 instance的原型链上

          • SuperClass.prototype是否出现在 instance的原型链上

          然后,对照本文中列举的一些图,一目了然就可以看清关系。有时候,完全没有必要弄的太复杂。

          觉得本文对你有帮助?请分享给更多人

          前端开发者丨JavaScript